Skip to main content

ADR: Unified Signature Model

Status

Accepted — implemented in migration 20260617120000_unify_signatures.

Context

Three parallel signing mechanisms existed:

  1. PnpSignature — SHA-256 hash, IP/UA, immutable issuedAt, assignment-bound.
  2. AgreementVendor.signedAt — nullable timestamp only; no hash, no metadata, no re-sign guard.
  3. Generic Document + Signature — weak hash (docId-userId-timestamp), no audit metadata.

This divergence created an inconsistent security posture and made it impossible to enforce uniform audit requirements across document types.

Decision

Replace all three with a single polymorphic Signature table backed by DocumentVersion:

  • DocumentVersion (document_versions) — one row per uploaded version of a signable subject. subjectType + subjectId + versionNumber uniquely identifies it. Signatures bind to a version, so they survive subject edits without becoming stale.

  • Signature (signatures) — unified table for all attestations. Polymorphic on subjectType (POLICY_PROCEDURE | BUSINESS_ASSOCIATE_AGREEMENT) and polymorphic signer via signerType + userId/vendorId (mirroring AuditLog.userId/actorVendorId).

Key invariants:

InvariantEnforcement
File-byte hashcontentHash = SHA-256(fileBytes) stored on every new signature
Signature hashhash = SHA-256(subjectType:subjectId:versionId:signerType:signerId:contentHash:issuedAt) — tamper-evident
No re-signTwo partial unique indexes (signatures_subject_user_uq, signatures_subject_vendor_uq) enforce one signature per signer per subject
ImmutabilityissuedAt is an ISO string frozen at write time; Signature has no updatedAt
"Exactly one signer"Enforced at service layer on insert (not a DB CHECK, which conflicts with onDelete: SetNull)
Cascade deleteSignature.versionIdDocumentVersion CASCADE; subjects must manually deleteMany from document_versions before deletion (enforced in PnpDocumentsRepository.delete and AgreementsService.delete)

The generic Document signing flow (POST /documents/:id/sign, GET /documents/:id/signatures) was vestigial and has been removed.

Consequences

  • Positive: one place to audit all signatures; uniform hash/metadata standard; re-sign blocking enforced at DB level.
  • Positive: vendor-signed BAA now has full audit trail (IP, UA, hash) matching PnP quality.
  • Negative: polymorphic FK means no automatic cascade from subjects to versions — deletion must be done explicitly in service layer.
  • Legacy data: migrated with algorithm='legacy' and preserved original hashes/timestamps where available.