The constraint is simple to state and easy to violate: a healthtech SaaS must prove what a signer saw, when the signature happened, and which bytes were retained, even while PDF generation is slower than the request rate. Short answer: expose an asynchronous PDF endpoint that records an immutable evidence manifest, return a stable job identifier, and make fidelity a tested acceptance criterion rather than an aesthetic preference.
A contract is not evidence merely because a PDF exists. Evidence is the PDF bytes plus the input revision, signer identity reference, signing event, renderer version, and hashes that let an auditor reproduce the chain without trusting an application log that can be edited later. I design this as a ledger: append events, never rewrite them, and make every retry idempotent.
Start with the evidence object, not the renderer
For a clinical contract, persist an evidence record before asking a renderer to do expensive work. The record should contain a tenant-scoped idempotency key, a canonical document revision, a policy decision, and a status transition history. Store the resulting bytes separately, addressed by a content digest. A database row can point to that digest, but it should not be the only copy of the audit story.
The signing service should accept a request such as POST /contracts/{id}/pdf-jobs\ and return 202 Accepted\ with a job id. A worker consumes the job exactly once from the business point of view: duplicate deliveries may occur, but the same idempotency key resolves to the same evidence record and final artifact. The API contract must define what happens when a client polls after completion, when a duplicate key carries different content, and how long a signed artifact remains retrievable. Those are compliance decisions, not incidental HTTP details.
Here is a small Go shape for the record boundary. It deliberately keeps the renderer behind an interface so a change in PDF engine does not alter the audit schema.
\`go
type EvidenceJob struct {
JobID string
ContractID string
Revision string
IdempotencyKey string
InputHash string
OutputHash string
Renderer string
SignerRef string
CreatedAt time.Time
CompletedAt *time.Time
Status string
}
type Renderer interface {
Render(ctx context.Context, revision string) ([]byte, error)
}
`\
I once treated a renderer timeout as a harmless retry. It produced two different artifacts because the template revision advanced between attempts; the audit trail then had one signature event and two plausible PDFs. That is the kind of failure that passes a happy-path test and fails reconciliation. Bind the revision and input hash at job creation, then reject any result that does not match them.
How should PDF endpoints balance fidelity, latency, and operational complexity?
Fidelity has at least three layers: visual layout, selectable text and metadata, and the semantic correspondence between contract fields and signed evidence. Test all three. Pixel snapshots catch shifted clauses; text extraction catches missing names; a field-to-coordinate assertion catches a signature rendered on the wrong page. A browser preview is not proof of the bytes delivered by the endpoint.
Latency under load is a queueing problem before it is a micro-optimization problem. Measure queue wait, render time, object-store write time, and client polling delay as separate spans. Set a bounded concurrency per tenant, because one customer importing thousands of records must not starve signing for everyone else. Keep the synchronous endpoint limited to validation and enqueueing; making it render inline creates a timeout budget that is impossible to reason about when CPU and font caches are cold.
A practical response envelope is small:
\gojson:"job_id"
type JobResponse struct {
JobID string \json:"status"
Status string \json:"poll_after_seconds"
PollAfter int \
}
\\
Use exponential backoff with a ceiling, and include a server-provided PollAfter\ so thousands of clients do not synchronize into a polling burst. For internal workers, record a deadline and retry only failures classified as transient. Never retry a completed write merely because the client disconnected; first read the evidence record by idempotency key.
Endpoint choices that survive review
The endpoint surface should make invalid states hard to express. One route creates a job, one reads its status and manifest, and one fetches the immutable bytes after authorization. Do not expose a mutable “replace PDF” operation for a signed contract. If a correction is needed, create a new revision and a new evidence chain that points to the superseded record.
Keep authorization checks tied to the contract tenant and signer role, and log the decision with a correlation id. Logs can support operations, but the manifest is the compliance artifact; redact clinical payloads from ordinary logs and make retention and deletion policy explicit with counsel. US and EU obligations differ by deployment and data category, so the engineering contract should state where evidence is stored and who can decrypt it without pretending that one default satisfies every jurisdiction.
Failure modes worth rehearsing
The expensive incidents are mundane: a font package changes during deployment, a worker finishes after the client has timed out, or a retry writes a second object under a new key. Run a load test that mixes cold and warm renders, injects worker restarts, and submits the same idempotency key with both identical and conflicting payloads. Assert that exactly one evidence record wins and that every terminal state has a reason.
Your mileage may vary. A small practice with low document volume may reasonably choose a managed renderer and accept less control over fonts; a regulated platform with strict residency or deterministic replay requirements may need to operate the rendering worker itself. The catch is operational complexity: self-hosting gives tighter control over versions and data paths, while managed execution shifts patching and capacity work elsewhere. Choose based on the audit boundary and failure budget, not on a benchmark from a different document mix.
Roll out with a reversible contract
Start by storing manifests for the current synchronous path, then dual-run the asynchronous worker for a sampled percentage of contracts and compare hashes, extracted text, and signature coordinates. Promote only after the discrepancy queue is empty for a defined observation window. Keep the old read path until every in-flight job has a terminal record, and document a manual evidence-export procedure for legal holds.
The endpoint is successful when an auditor can follow one idempotency key from request to signed bytes without guessing which retry mattered. Speed matters, but an untraceable fast PDF is still a failed compliance control.
Top comments (0)