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.

Status: v1, in progress. The envelope lifecycle, signer tracking, provider-driver seam, and signature-verified webhooks are all wired end-to-end, and the Demerzel signing UI composes envelopes and manages providers — but the only driver shipping today is a 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, a signedMediaAssetId (the executed PDF recorded back into Media). Carries the providerKey + providerEnvelopeId (the upstream id, and the webhook routing key) and a status. Lifecycle: DRAFT → SENT → PARTIALLY_SIGNED → COMPLETED, with DECLINED and VOIDED branches.
Signer
One recipient on an envelope — an optional Terminus terminusPersonId plus a denormalised name and email (the email is the webhook join key), a role, and a signingOrder. Status moves PENDING → SENT → VIEWED → SIGNED, with a DECLINED branch that captures the reason.
Provider-driver seam
One contract — SignatureProviderInterface with createEnvelope / void / verifyWebhook / parseWebhook — the same shape Payments and Elliot use. Drivers auto-register and resolve by provider type. A MockSignatureProvider ships today; DocuSign, DropboxSign, and AdobeSign are the roadmap and each lands as a new driver class, no core change.
Provider configuration
SignatureProviderConfiguration is a tenant's account with a vendor — type, active flag, a priority for default routing, and a settings blob 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.

Sign endpoints in the Foundation API reference OpenAPI 3.1 schema for Sign with request/response shapes, parameters, and a try-it client.

Quick reference

MethodPathPurpose
POST / GET/sign/v1/envelopesCreate 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}/voidCancel an envelope upstream (DRAFT / SENT / PARTIALLY_SIGNED only).
POST / GET / DELETE/sign/v1/provider-configurationsManage 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.