Flatten patient invoice PDFs for final delivery, but retain an access-controlled editable source when corrections are part of the operating model. The deciding constraint is template ownership: the team that owns field names, validation rules, and release timing can safely preserve editability upstream; a downstream team receiving an opaque template should treat flattening as a controlled finalization step.
Short answer: do not make one file serve as both working state and immutable-looking output. Generate both artifacts from the same order snapshot, attach a template revision and content digest, and expose only the artifact appropriate to the workflow. Editable fields help billing staff correct approved data. Flattened delivery reduces accidental changes and rendering surprises, but it is not redaction, encryption, or proof that a document was never altered.
Should you flatten a PDF form or keep it editable after filling?
An editable PDF form carries a small schema inside a presentation file: field names, types, values, appearance information, and sometimes calculations or scripts. ISO 32000-2 defines PDF, including interactive forms, but a standard file format does not resolve who owns that schema in your organization. That is the operational question.
For a small healthtech SaaS generating invoice PDFs from order data, ownership can be explicit. Billing owns the invoice meaning. Engineering owns the order-to-document mapping and release process. Compliance or legal review may constrain the displayed language. Support may be allowed to request a correction, but should not silently rewrite a delivered invoice. If nobody can name the owner of a field such as patient_balance, keeping the customer-facing copy editable turns an unclear organizational boundary into a data-quality risk.
The choice is asymmetric. When your team controls the template, a versioned editable master can be useful. When a clinic, insurer, or design contractor supplies the template and may replace it without coordinating field changes, fail closed on unknown or missing fields and flatten only after validation. Do not guess. A blank amount that still produces a polished page is worse than a failed job because it can cross the delivery boundary unnoticed.
This is the central trade-off.
| Condition | Working artifact | Delivered artifact | Reason |
|---|---|---|---|
| Billing correction is allowed before approval | Editable, access-controlled PDF | Flattened PDF after approval | Separates review from delivery |
| Recipient must enter information | Editable PDF | Editable PDF | Interactivity is the requirement |
| Template schema changes without coordination | Quarantined until mapping passes | Flattened PDF | Prevents silent field drift |
| Invoice is final and no recipient input is expected | Optional editable source | Flattened PDF | Narrows accidental-edit surface |
This policy does not make a flattened PDF immutable. A recipient can still modify bytes with suitable tools. If authenticity matters, use an appropriate digital-signature process and define which later changes are permitted.
Flattening is unsuitable when the recipient must complete the form. Keeping fields editable is unsuitable when the file is supposed to represent an approved, final invoice and no further input is expected. Neither mode supplies redaction or authenticity, and an editable archive also carries a storage and access-control drawback: it preserves another copy of sensitive working data that must be governed for its full retention period.
Model finalization as a state transition
Treat editable and flattened as states, not export preferences in a UI. A job starts from a stable order snapshot, selects an approved template revision, fills named fields, validates the result, and then finalizes it. Delivery happens after that transition.
Keep the state machine small:
-
prepared: the input snapshot and template revision are fixed. -
filled: values are mapped, but form fields remain interactive. -
finalized: field appearances are rendered into page content by the selected PDF implementation. -
delivered: the selected artifact and its digest are recorded against a destination.
Never let a retry move backward from delivered to filled. Queue workers retry. Webhooks repeat. Operators replay jobs during incident recovery. The job key should represent the business operation, such as invoice ID plus invoice revision, rather than a random attempt ID. The resulting digest answers a practical question: is this the same artifact already sent?
The following Go boundary keeps policy outside the PDF implementation. It deliberately does not pretend that byte concatenation or a print operation can flatten a form correctly; a conforming PDF processor belongs behind the interface.
package invoice
import (
"context"
"crypto/sha256"
"fmt"
)
type FinalMode string
const (
KeepEditable FinalMode = "editable"
Flatten FinalMode = "flattened"
)
type Input struct {
InvoiceID string
InvoiceRevision int
TemplateID string
TemplateRevision int
Fields map[string]string
}
type Artifact struct {
Bytes []byte
Mode FinalMode
Digest [32]byte
}
type PDFProcessor interface {
Fill(context.Context, string, int, map[string]string) ([]byte, error)
Validate(context.Context, []byte, []string) error
Flatten(context.Context, []byte) ([]byte, error)
}
func Build(ctx context.Context, p PDFProcessor, in Input, mode FinalMode, expected []string) (Artifact, error) {
filled, err := p.Fill(ctx, in.TemplateID, in.TemplateRevision, in.Fields)
if err != nil {
return Artifact{}, fmt.Errorf("fill invoice %s: %w", in.InvoiceID, err)
}
if err := p.Validate(ctx, filled, expected); err != nil {
return Artifact{}, fmt.Errorf("validate invoice %s: %w", in.InvoiceID, err)
}
output := filled
if mode == Flatten {
output, err = p.Flatten(ctx, filled)
if err != nil {
return Artifact{}, fmt.Errorf("flatten invoice %s: %w", in.InvoiceID, err)
}
}
return Artifact{Bytes: output, Mode: mode, Digest: sha256.Sum256(output)}, nil
}
A production input also needs currency, locale, billing address, and typed line items. They are omitted because the important boundary here is the finalization contract. Validate typed invoice data first, then map it to PDF field strings at the template adapter.
Catch the failures that look successful
A renderer returning bytes is a weak success signal. The dangerous failures are valid-looking files with wrong semantics: a renamed field remains blank, a long procedure description clips, a currency value uses the wrong locale, or an appearance is absent in one viewer. Flattening can preserve the wrong result perfectly.
Green is not enough.
Verification should happen at three levels. First, validate business input before generation: invoice identity, revision, line totals, currency, and required recipient data. Second, validate the form mapping against the exact template revision. Reject missing required fields and unexpected template changes. Third, inspect the produced document as a recipient would. Open it with the viewer families customers actually use, render pages in tests, and compare selected fixtures after deliberate review. Pixel comparisons need tolerances and stable fonts; otherwise environment noise becomes an alarm source.
Parse the finalized artifact and assert that fields intended for recipient input still exist in editable mode. For a flattened invoice, assert that those interactive fields are absent or no longer editable according to the chosen processor, then verify that visible values survived by rendering the page. One assertion without the other misses half the failure.
Be careful with sensitive data in observability. Log invoice IDs, revisions, template revisions, transition names, durations, byte sizes, and digests. Do not log patient names, addresses, form values, or raw PDF bytes. Metrics should distinguish fill, validation, finalization, storage, and delivery failures because their owners and rollback actions differ. A single pdf_failed counter is almost useless at 02:00.
Alert on finalized jobs that do not reach delivery within the service objective, repeated delivery attempts for the same invoice revision, and a rise in template-validation failures after a template release. The thresholds depend on the business schedule and traffic; inventing universal numbers would make the runbook less reliable.
Deployment, retry, and rollback
Release templates as immutable revisions. Never replace the bytes behind an existing revision identifier. A deployment should include the template, its expected field manifest, representative order fixtures, and approved render snapshots. Promote that unit through environments together, then route a controlled portion of new jobs to it.
Rollback means routing new work to the previous approved template revision. It does not mean regenerating already delivered invoices behind the same invoice revision. If a delivered document is materially wrong, create a corrected business revision and preserve the relationship between the original and correction. That keeps retries idempotent and gives support a defensible trail.
Duplicate delivery deserves its own guard. Before sending, atomically claim the tuple (invoice_id, invoice_revision, destination, artifact_digest). If the exact tuple is already marked delivered, acknowledge the retry without sending again. If the invoice revision matches but the digest differs, stop the job and investigate; the supposedly deterministic generation path has diverged.
Stop there.
Do not flatten in the request path merely because the library call appears fast. Put generation behind a durable job when delivery can outlive an HTTP request, and make status visible to the calling application. Store the source snapshot and identifiers needed to reproduce policy decisions, subject to retention rules. Store the editable artifact only if the correction workflow needs it. Extra artifacts expand access-control, retention, and deletion obligations.
The practical rule
Use an editable PDF when a named actor must enter or correct data and the template schema has a clear owner. Use a flattened PDF for final invoice delivery when no recipient input is expected. Generate it only after semantic and visual checks, and keep the working artifact separate from the delivered one.
Flattening is a presentation-state transition. It does not redact hidden content, encrypt the file, authorize a correction, or establish authenticity. Those require separate controls. For a small SaaS, this separation keeps the incident question answerable: which order revision, template revision, policy, and exact bytes produced the invoice the recipient received?
References
- ISO 32000-2, Portable Document Format: https://www.iso.org/standard/75839.html
- NIST FIPS 180-4, Secure Hash Standard: https://csrc.nist.gov/pubs/fips/180-4/upd1/final
- Go package
crypto/sha256: https://pkg.go.dev/crypto/sha256
Top comments (0)