Primitive spec
Sign
The e-signature primitive. Sign sends a document out for signature, tracks each signer, and records the completed file back into Media — with the actual e-sign vendor (DocuSign, Dropbox Sign, Adobe) sitting behind one provider-driver contract, exactly like Payments processors and Elliot carriers.
Mock provider (no external send). The real DocuSign / Dropbox Sign / Adobe Sign drivers are the next slice; the pill moves to Live when the first one lands.
What it owns
Sign owns the signing ceremony: which document goes out, who must sign, what order, and where each signer is in the flow. It is a thin, vendor-neutral layer — the SignatureEnvelope is the platform's record of a signing, while the heavy lifting (rendering fields, hosting the signing page, the legal audit trail) happens at the provider.
Sign does not store the document bytes (that is Media), own the signer's identity (Terminus), or send the "please sign" notification itself (that is Speaker, on the outbox event). It carries opaque references to all three.
Concepts
- SignatureEnvelope
- The signing aggregate. Points at a
sourceMediaAssetId(the Media document to sign) and, on completion, asignedMediaAssetId(the executed PDF recorded back into Media). Carries theproviderKey+providerEnvelopeId(the upstream id, and the webhook routing key) and a status. Lifecycle:DRAFT → SENT → PARTIALLY_SIGNED → COMPLETED, withDECLINEDandVOIDEDbranches. - Signer
- One recipient on an envelope — an optional Terminus
terminusPersonIdplus a denormalised name and email (the email is the webhook join key), arole, and asigningOrder. Status movesPENDING → SENT → VIEWED → SIGNED, with aDECLINEDbranch that captures the reason. - Provider-driver seam
- One contract —
SignatureProviderInterfacewithcreateEnvelope/void/verifyWebhook/parseWebhook— the same shape Payments and Elliot use. Drivers auto-register and resolve by provider type. AMockSignatureProviderships today;DocuSign,DropboxSign, andAdobeSignare the roadmap and each lands as a new driver class, no core change. - Provider configuration
SignatureProviderConfigurationis a tenant's account with a vendor — type, active flag, apriorityfor default routing, and asettingsblob whose credentials are encrypted at the Doctrine boundary.- Webhooks
- Providers call back to
POST /sign/v1/webhooks/{providerConfigurationId}; the driver verifies the vendor's HMAC signature over the raw body, parses the events, and advances each signer and the envelope (forward-only). On completion the signed document is pulled back into Media, and each status move emits an outbox event — the seam a Speaker subscriber will use to notify the next signer.
API surface
All endpoints are versioned under /sign/v1/, return RFC 7807 problem details on error, and read tenantId from the bearer token. The webhook route is public — the config id routes it and the provider's signature authenticates it.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /sign/v1/envelopes | Create an envelope (document + signers) and send it, or list envelopes by status. |
| GET | /sign/v1/envelopes/{id} | Fetch an envelope with its signers and source / signed document URLs. |
| POST | /sign/v1/envelopes/{id}/void | Cancel an envelope upstream (DRAFT / SENT / PARTIALLY_SIGNED only). |
| POST / GET / DELETE | /sign/v1/provider-configurations | Manage tenant e-sign vendor accounts (encrypted credentials). |
| POST | /sign/v1/webhooks/{providerConfigurationId} | Provider callback — HMAC-verified, advances signer + envelope status, records the signed PDF. |
Example: send for signature
Point an envelope at a Media document and list the signers; the provider takes it from there:
POST /sign/v1/envelopes
Content-Type: application/json
Authorization: Bearer <token>
{
"title": "Membership agreement",
"mediaAssetId": "med_01JC…",
"signers": [
{ "terminusPersonId": "per_01JAB7…", "name": "A. Member", "email": "a@example.com", "signingOrder": 1 }
]
}
→ 201 Created sge_01JD… status: SENT
// provider webhook, later
POST /sign/v1/webhooks/spc_01J… { signer signed → envelope COMPLETED }
→ 202 Accepted { "updated": 1 }
signed PDF recorded back to Media as med_01JE…
How it fits with the rest
flowchart LR
Md[Media source PDF] -. med_* .-> Sg(Sign)
Sg -- createEnvelope --> Prov[Provider driver]
Prov --> Mock[Mock]
Prov --> Real[DocuSign / Dropbox Sign / Adobe]
Webhook[Provider webhook] -- signer / envelope status --> Sg
Sg -- signed PDF --> Md
Sg -. signer .-> Te[Terminus]
Sg -. notify next signer .-> Sp[Speaker]
Sign sits on top of Media in both directions — it reads the document to sign and writes the executed one back — and references Terminus for signer identity. Status changes land on the audit outbox, the same seam Speaker uses elsewhere, so a consent-gated "your turn to sign" notification is a subscriber away. The e-sign vendor is swappable: it is one driver behind the provider seam, mirroring how Payments treats processors and Elliot treats carriers.