E2E covers backend, web, Android and the iOS simulator suite, see CI/CD.
Embedded e-signing for React Native and React web apps. One ESignature
component, three integration modes - you only need the parts for your
mode, and for two of the three that is a single small package:
| Mode | What it is | What your app installs | Backend required |
|---|---|---|---|
| 1. Public URL | A published public form URL embedded directly |
One package via the Apollo-free /webformentry - no Apollo, no GraphQL |
None |
| 2. Web Forms instances |
Prefilled per-signer forms (read-only fields locked); your backend mints an instance URL with one API call |
Same minimal /webformentry |
One mint endpoint, either tier: in-process with @blinkbitcoin/esign-node,or deploy @blinkbitcoin/esign-service(no database) |
| 3. Envelope | The signer opens the agreement itself, minted from your template(s) with your values locked on the document; with a database, full orchestration: per-recipient sessions, restart on expiry, webhook status sync |
The mint: the same minimal entry (your app calls one endpoint). Orchestration: the package + @apollo/client+ graphql |
The mint: @blinkbitcoin/esign-serviceunder ESIGN_MINT_MODE=envelope(no database), or the envelope preset of @blinkbitcoin/esign-nodein your own Node API. Orchestration: the service with DATABASE_URL, or the envelopedomain of esign-node |
The GraphQL API and the Apollo wiring exist for mode 3's orchestration only. If you need modes 1 or 2, or mode 3's envelope mint, none of that ships with you: the mint runs either inside your own Node API or in this repo's service, with no database at all (Backend options). The Integration section walks each mode from simplest up.
Which mode? Nothing to lock and no per-signer data: mode 1. Values the
signer must not change (amounts, rates, dates set by you): mode 2 when a
form should ask the signer the rest first, mode 3's envelope mint when the
signer should land on the agreement itself - both lock fields, both need
one call on your backend and no database. A document workflow with
per-recipient sessions, restarts and status tracking: mode 3 with a
database. Reading path for locked terms:
docs/integration/locked-prefill.md (the
Web Forms recipe, backend + app) or
docs/integration/locked-prefill-envelopes.md
(the envelope recipe) → docs/integration/docusign-lessons.md
(the rules, one page) → docs/integration/webforms.md
(the details) → examples/mint-only-demo
(the API side, runnable).
Who are you? Three paths through this repository:
| You are | Your path |
|---|---|
| App developer / integrator |
Integration - the three modes, same component consuming.md - registry setup and the minimal install locked-prefill.md - the mode 2 recipe, backend + app |
| Backend developer |
The in-process mint preset - routes in your own APIexamples/mint-only-demo - a runnable API that mintsRunbook: backend developer - what to build once |
| DevOps engineer |
Backend options - the two tiers, side by side Deploy table - the copy-paste per target Runbook: DevOps - env, keys, boot guard, health |
Every mode drives the same component with the same callbacks - the only
thing that changes is the SigningSource you pass in:
<ESignature source={source} onComplete={…} onError={…} onCancel={…} />The modes below go from simplest to most capable. Start with the first one that covers your needs.
Use when: every signer gets the same form and you don't need per-signer prefill - you just publish the form in the DocuSign Web Forms builder and embed its public URL.
Install the package and the two WebView peers - nothing else:
npm i @blinkbitcoin/esign-react-native react-native-webview @react-native-community/netinfo
# note: no @apollo/client, no graphql - not needed for modes 1 and 2import { ESignature, createPublicUrlSource } from '@blinkbitcoin/esign-react-native/webform';
const source = createPublicUrlSource({ url: 'https://your-published-form-url' });That's the whole integration: the component embeds the URL in a WebView and your callbacks fire on completion/cancel/error.
Use when: you want each signer's data prefilled into the form, need values the signer cannot change (the fields marked read-only in the builder show the minted values locked), or need to know which signer completed it. DocuSign requires minting a short-lived instance URL per signer, and that API call carries your DocuSign credentials - so it belongs on a backend, not in the app. One rule from the live runs: locked fields must be Text (or Dropdown) fields fed strings - a read-only Number or Date field makes DocuSign refuse the submission (lessons).
- Add one authenticated endpoint to your own backend that calls
DocuSign's
createInstancewith the signer'sclientUserId+ prefill values and returns{ url }- one call from@blinkbitcoin/esign-node(createWebFormInstance) in any Node backend, or this repo's service, which exposes exactly that asPOST /webform/instance. - Install exactly as in mode 1 (same minimal packages, still no Apollo).
- Point the source at your endpoint:
import { ESignature, createWebFormsSource } from '@blinkbitcoin/esign-react-native/webform';
const source = createWebFormsSource({
// your endpoint + the app's own session token; the backend mints with
// @blinkbitcoin/esign-node, so read-only fields come back locked
mint: { url: 'https://your-backend.example.com/webform/instance', getAuthToken },
prefill: { number_of_units: '1000', settlement_amount_btc: '0.01268231' }, // locked fields: strings
});
// A GraphQL mutation instead of a POST endpoint: createWebFormsSource({ createInstance })Completion reaches the app through the instance's return URL: your backend
serves the small bridge page (renderSigningReturnBridge) that the
component listens to - no DocuSign.js, works in a plain WebView. The whole
recipe, backend and app: docs/integration/locked-prefill.md.
Modes 1 and 2 import from the /webform subpath, which is Apollo-free by
construction (a guard test walks the import graph to keep it that way).
When installing from GitHub Packages add --omit=peer, otherwise npm also
drops the unused Apollo peers into node_modules - the registry omits
peerDependenciesMeta (details in
docs/integration/consuming.md).
Web Forms specifics - event model, real-DocuSign caveats:
docs/integration/webforms.md.
Use when: you need real envelope workflows: creation from DocuSign
templates, a distinct session per recipient, session restart after expiry,
and webhook-driven status tracking in a database. This is the mode the rest
of this repo exists for - packages/esign-service (GraphQL service, provider adapters,
webhooks) plus the Apollo client wiring.
- Deploy this repo's backend (packages/esign-service).
- Install the package plus the Apollo peers:
npm i @blinkbitcoin/esign-react-native react-native-webview @react-native-community/netinfo \
@apollo/client graphql- Wire the client and source from the package root (not
/webform):
import {
ESignature,
createESignApolloClient,
createProxySigningSource,
} from '@blinkbitcoin/esign-react-native';
import { ApolloProvider } from '@apollo/client/react';
const client = createESignApolloClient({
uri: 'https://your-backend.example.com/graphql',
getAuthToken: () => readTokenFromSecureStorage(),
});
const source = createProxySigningSource({
client,
contractType: 'loan_agreement',
recipient: { name, email },
});
<ApolloProvider client={client}>
<ESignature source={source} onComplete={…} onError={…} onCancel={…} />
</ApolloProvider>;The web package (@blinkbitcoin/esign-react) mirrors all of the above for
React DOM apps (iframe instead of WebView), and adds a DocuSign.js source
for real Web Forms embedding on web - see its
README.
Packages publish to GitHub Packages under the blinkbitcoin org - registry
setup: docs/integration/consuming.md.
Independent of the signing mode above, pick how much of the screen the library draws. Same state machine in every column; the host takes over more from left to right.
| Default | Themed | Headless |
|---|---|---|
<ESignature source={source} … /> |
theme · styles · labels on ESignature |
useESignature + your own WebView / iframe |
Details and code for each path: the package READMEs (RN, web).
Modes 2 and 3 need a mint on a backend you control, and there are exactly two
tiers to choose between. The app code is identical for both - the same
SigningSource calls one endpoint and embeds the URL it gets back.
| Tier | What you run | What your API must provide | Capabilities | Copy-paste |
|---|---|---|---|---|
In-process@blinkbitcoin/esign-node |
The package inside your own Node API (router or Fetch handler) |
Your own session check ( authenticate), and thelocked terms from the prefill hook - which canreject a mint by throwing Errors.validationError |
Mint; the envelope domain too, over your own store |
The mint-only preset |
Deployable@blinkbitcoin/esign-service |
The package or theghcr.io/blinkbitcoin/esign-serviceimage, as a function or a container |
ESIGN_SESSION_JWKS_URL orESIGN_SESSION_SECRET(who the caller is), plus ESIGN_PREFILL_URL when thelocked terms come from your data |
Mint always on; envelopes, webhooks and GraphQL with DATABASE_URL |
Deploy table |
Mode 2 needs no database with the service: the mint is always on, and
DATABASE_URL only adds the envelope half. Deploy targets are a Node
container, Vercel, a Cloudflare Worker (mint only), Kubernetes, or Lambda via
the same image - one row each, with the commands, in the service's
Deploy table.
Taking either tier live - DocuSign go-live, the environment, the private key per platform, the boot guard and the verification checklist - is the runbook: docs/operations/production.md.
Ordered by how likely you are to need each part:
| Path | What lives there |
|---|---|
packages/esign-react-native/ |
The React Native library you install: the ESignature component and the signing sources. |
packages/esign-react/ |
The React web library: the same component and sources for browser apps, embedding with an iframe instead of a WebView. |
packages/esign-core/ |
The shared core both libraries build on: the SigningSourceabstraction and event interpreters, plus the GraphQL client pieces used by mode 3. It arrives automatically as a dependency - you never install it directly. |
packages/esign-node/ |
The Node-only server half for your own backend: mint Web Forms instances with locked prefill in one call, or run the whole envelope domain (mode 3) over your own store, as a Fetch handler or an Express router. The service below is built on it. |
examples/mint-only-demo/ |
Server shape for most hosts: your existing API adds one mutation that mints a locked Web Forms instance. |
examples/serverless-handler-demo/ |
Server shape for route handlers and edge functions: the package's mint and webhook handlers, Request → Response. |
packages/esign-service/ |
The whole service as one deployable: it always mints, and adds the GraphQL API, the provider webhook and the PostgreSQL store when DATABASE_URL is set. Needed onlywhen you deploy a backend rather than mint from your own. |
examples/react-native-demo/ |
A complete React Native app hosting the component. Used for manual testing, and the mobile end-to-end suites drive it. |
examples/react-demo/ |
The same for the browser: a small React app hosting the web component, driven by the browser end-to-end suites. |
docs/ |
Documentation of how everything currently works - start at docs/index.md; upgrading notes in docs/upgrading.md. |
scripts/ |
The CI / E2E / release shell and node the Makefile and the workflows run; its logic is a tested tooling workspace. |
Only needed if you're working on the packages or the backend themselves - consuming the packages requires none of this (see Integration above).
One-time setup:
make install # npm ci across all workspaces (also installs git hooks)
direnv allow . && direnv allow packages/esign-service # once per machine (loads env + nix flake dev shell)Working on the libraries requires nothing else - no backend, no database. The unit suites, lint, and typecheck run standalone:
make testRunning the demo apps is where the backend comes in: the demos sign against a locally running service and its Postgres. By default it uses the mock provider, so no DocuSign account or credentials are needed:
make db-up migrate backend # dev Postgres + migrations + server (:4100 in the main clone;
# a worktree gets its own port block - `make ports` shows it)
# in a new terminal:
make start # Metro
make ios # or: make android
make web # or the web demo (Vite)Real DocuSign stays opt-in: configure credentials per
docs/integration/docusign-proxy.md, then make test-live
verifies the API contracts against a demo account (it skips itself when no
credentials are set).
make help lists all targets; the table below is the everyday subset. Every
CI job that can run on a laptop has one, and the workflows call it rather than
the script underneath - make check-ci fails if those two ever disagree
(see Running CI Locally):
| Target | Purpose |
|---|---|
make test |
Unit suites + lint + typecheck + format check |
make unitmake coverage |
Test suites (100% coverage on packages + backend + scripts/lib) |
make coverage-badge |
Coverage badge + HTML report from the last make coverage run |
make check-code |
Lint + typecheck + format check only |
make build |
Build the packages (bob for RN, tsup for core/node/web, tsc for the service) |
make e2e-backendmake e2e-web |
Backend / browser E2E: test DB up → migrate → tests → teardown (e2e-webbuilds the libraries first and bundles the demo against their dist) |
make e2e-iosmake e2e-android |
Maestro E2E against a running stack |
make e2e-ios-localmake e2e-android-local |
The whole mobile stack in one command (DB, backend, .app or APK, Metro, Maestro, teardown); iOS boots a simulator, Android needs a running emulator. The steps: e2e-backend-up, ios-build /android-build, e2e-metro-up, e2e-ios / e2e-android |
make check-ci |
Lint the CI itself: actionlint on the workflows, shellcheck onscripts/**, the dependency audit, and check-parity - theworkflows must call the make targets, not repeat their commands |
make check-packages |
Package shape of the built packages: publint + arethetypeswrong + the pack/install smoke |
make docker-smoke |
Build the service image and boot it in both modes (mint only, then with Postgres), as CI's Docker job does |
make e2e-server-demos |
Boot the mint-only and serverless examples on the mock provider and call their routes |
make portsmake ports-free |
This worktree's port block (every service, both databases, Metro) and who holds each port; stop what this worktree left on them |
make test-live |
Opt-in live DocuSign API verification (skips without credentials) |
make e2e-livemake e2e-ios-live |
Live journeys against real DocuSign (needs make docusign-env): thelocked Web Form submitted and signed inside the web component + a proxy-mode signature; the same Web Form journey in the React Native demo's WebView (booted simulator) |
make live-webmake live-iosmake live-android |
The web demo / the RN demo on the attached phone against real DocuSign, interactive: .env, a Tailscale Funnel public URL forConnect webhooks, the service, the demo; waits for the manual rows of docs/integration/docusign-proxy.md section 5, Ctrl-C tears down |
make pods |
iOS CocoaPods install |
Coverage is 100% everywhere, the demo apps included. The HTML report
lands in coverage/report/index.html; CI publishes
the badge per branch to gh-pages/badges/<branch>/ and uploads the report as
the coverage-report artifact of every run.
See docs/development-guide.md for full setup, environment variables, and troubleshooting.
- docs/index.md - the map of every page, by what you are doing
- docs/integration/ - using the packages: registry, the three modes, locked terms, error codes, DocuSign
- docs/architecture/ - how it works inside, security, the nine diagrams
- docs/operations/ - the live DocuSign job in CI; releasing and upgrading
- CONTRIBUTING.md - Conventional Commits (enforced by hooks and CI), the quality gates, the PR checklist
- SECURITY.md - reporting a vulnerability privately
- LICENSE - MIT