How a case document would travel from a blank Kearney AcroForm to a signed, stored PDF, with Stirling filling it and DocuSeal collecting signatures. Every timing on this page was measured in the spike, not guessed.
ESignature:: code exists yet.submission.completed webhook arrivesFive actors. Read top to bottom. Each box is a step; the lane shows who performs it. Steps are numbered to match the detail below.
form/fields-with-coordinates returns widget rects (top-left, points)document_templates.signer_rolesform/fill ~0.16 smisc/flatten forms-only ~0.14 sPOST /submissions/pdf returns submitter ids + signing links ~0.8–1.2 sPENDING_SIGNATURE. Response to FD.submission.created webhook < 1 s<docuseal-form> (inline, not an iframe)form.viewed → form.started → form.completed (with IP + device)GET /submissions/:idsubmission.completed ~1–2 stenants/<tid>/signed_documents/… via Storage::SIGNED; artifact lifecycle signed satisfies the readiness requirement (ADR-061)GET /submissions/:id for anything still pending) catches lost or late webhooks. See step 16 and the gotchas.Times are from the spike: LAN Stirling to DocuSeal Cloud (US), from Philip's PC. On Railway expect small network differences; a self-hosted DocuSeal next to app-core would make steps 8 and 17 faster.
When a template is registered, call Stirling form/fields-with-coordinates on the blank. Keep the widgets typed signature (plus initials/date widgets) and map each to a signer role by its name. Kearney's names already carry the role, e.g. disposition_authorizer_signature. Store the rects on signer_roles, which today holds role-name strings only, so this is a schema change.
app-core resolves who signs what: authorizer, purchaser, FD witness, and so on. It takes names and emails from the case, plus the section choice for conditional forms. Several documents for the same case can go in one submission, giving one packet, one audit log and one $0.20 fee.
This is the shipped Documents::Generator pipeline: S3 blank → form/fill → QR stamp if the template carries one → misc/flatten. The proposed change is to call the flatten with flattenOnlyForms=true (forms-only) for documents that go to signing.
For each role's rect: x = rect.x / page_width, y = rect.y / page_height (same for w, h), page = pageIndex + 1. Both Stirling and DocuSeal use a top-left origin, so there is no y-flip. The flip is only for Stirling's own stamp endpoints, like the QR code.
# one DocuSeal field per signer box { "name": "authorized_person_signature", "type": "signature", "role": "Authorized Person", "required": true, "areas": [{ "x": 0.5319, "y": 0.6540, "w": 0.3791, "h": 0.0228, "page": 2 }] } # signed-date boxes: filled automatically at signing, never by us { "name": "signed_day", "type": "date", "role": "Purchaser", "readonly": true, "default_value": "{{date}}", "preferences": { "format": "DD" }, "areas": [ … ] }
POST /submissions/pdf with the produced PDF (base64), the fields, and one submitter per role. Put our signature-request id in each submitter's external_id. The response returns each submitter's id and signing link (embed_src). Save them, and mark the document PENDING_SIGNATURE. DocuSeal then fires submission.created.
send_email: true lets DocuSeal email the links. Alternatively we send our own branded email or SMS with the link.Remote: the family opens the link on their own phone or computer. In person: our React page mounts DocuSeal's <docuseal-form> web component with the signer's link. It renders inline, not in an iframe, and works on an iPad (verified: ~14 s to initial twice and sign). As each signer finishes, DocuSeal sends form.completed with their IP and device, and we update that signer's status.
When the last signer finishes, DocuSeal seals the PDF with a digital signature and timestamp, builds an audit log (document hashes, IPs, devices), and sends submission.completed about 1–2 s later. Our endpoint:
X-Docuseal-Signature = hex HMAC-SHA256 of "<timestamp>.<raw body>" with the webhook's whsec_… secret (reject if older than 5 min);GET /submissions/:id before acting.Download the signed PDF and the audit-log PDF from the URLs in the submission and write them to S3 via Storage:: (ADR-007). Then set the document to SIGNED and drive the artifact lifecycle to signed, which satisfies the case's readiness requirement (e.g. cremation_authorization_signed, ADR-061).
| Situation | What DocuSeal does | What app-core should do |
|---|---|---|
| Signer declines | form.declined with data.decline_reason. Decline is only possible in the signing UI. | Mark the request declined, notify the FD, offer void and re-send. |
| Nobody signs in time | submission.expired at expire_at (fired on the second in the spike). | Mark expired; FD can re-send. |
| Our endpoint is down | Retries with doubling delays: ≈1, 2, 4, 8 min … (about 2.8 days in total). | Handler must be idempotent. The poll catches anything missed. |
| Events out of order | submission.completed can arrive before the last form.completed. | Never depend on order. Re-read the submission. |
| Signer tries to change prefilled data | Nothing to change: the PDF is flattened. With DocuSeal-side prefill, writes to another role's field are dropped. | Nothing extra. |
09/30/2026). Always set preferences.format.{{date}} prints blank. Whether {{date}} means the creation or the signing date is still being tested.fields expecting DocuSeal to merge them with auto-detected ones. It replaces them.Only steps 8, 10, 13, 15 and 17 talk to DocuSeal. Everything else (who signs, where boxes go, the flattened PDF, storage, case state) is ours. Keeping that line in ESignature:: (ADR-019) means a second provider for a tenant that insists on DocuSign would be an adapter, not a rewrite.
signer_roles gains rects, and the signing tables (signature_requests, signature_events) don't exist yet.