31. Onboarding agreements & e-signatures (hybrid)
Date: 2026-06-14
Status
Accepted
Context
Onboarding collected profile + documents but the contract-signing step was a placeholder
(OnboardingWizard showed "Contract e-signature (DocuSeal) will be added here before go-live").
The contractor_documents.docuseal_submission_id column existed but nothing used it. Contractors are
Philippine independent contractors signing a small set of simple agreements (IC, NDA); the owner
wanted in-app signing that's fully under our control, while keeping the door open to a formal audited
envelope (DocuSeal) for anything heavier later.
Decision
- Hybrid e-signature. Build native in-app signing for the simple PH agreements against an immutable signature ledger; keep DocuSeal available (the retained column) for formal envelopes later. No third-party dependency is required for the common path.
- Immutable ledger (
onboarding_signatures, migration 0027). One row per(contractor, kind, party). The contractor signs asparty='contractor'; an authorized admin counter-signs asparty='company'. RLS is SELECT-only for clients (admins see their company, a contractor sees their own) — there is no insert/update/delete policy, so the ledger is immutable except via service-role server actions. Every row is audited (app.audit_row). - Hash what was actually signed. Each signature stores
agreement_sha256=sha256(mergedBody \n signedName \n signedDate)(src/lib/agreements/hash.ts). The body is merged server-side at sign time from the template + the contractor's data (src/lib/agreements/merge.ts,{{token}}substitution) — the client never supplies the hashed text — so a later template edit can never change what a contractor already agreed to. - Templates with a safe default.
agreement_templates(per company, per kind) holds the body + merge tokens, edited by admins. When a company has no template for a required kind yet, the flow falls back to a built-in[PLACEHOLDER]default so onboarding is never blocked; the admin editor persists the real legal text before go-live. - Typed or drawn.
SignaturePadcaptures a typed name (rendered cursive) or a drawn PNG (bounded data URI); a scroll-to-bottom gate enables signing.method+ optionalsignature_dataare recorded. IP/User-Agent are captured server-side from request headers. - Document review workflow. Admins review uploaded docs (approve / waive / defer /
needs-replacement) with a reason and an optional
issued_on(e.g. NBI freshness); columns added tocontractor_documents. Counter-signing is gated byprofiles.can_countersign.
Consequences
- Onboarding can require the contractor to sign the configured agreements before submitting; the admin reviews documents and counter-signs, then activates.
- The signature ledger is court-defensible-ish (hash + name + date + IP/UA, immutable) without a third party; if a formal audited envelope is later required for a specific agreement, DocuSeal slots in via the retained column without disturbing the ledger.
- Editing a template does not alter past signatures (they keep their own hash + the body is re-derivable from the template version they signed against — future work: snapshot the body per signature if templates start changing frequently).
- New tables (
agreement_templates,onboarding_signatures) +contractor_documentsreview columnsprofiles.can_countersignare added tosrc/db/types.ts; regenerate after further changes.