Compliance & Trust — the nine unique features
Audit chain, idempotency, evidence completeness, suppression + WORM, RGPD, account deletion, status page, maintenance mode, security headers — the parts that turn a SaaS demo into a SaaS your CFO can sign off on.
Why this section exists
Most boilerplates stop at "auth + Stripe + a dashboard." When your buyer's legal team asks for an audit log, a data export, or a way to suppress noisy findings, you'd normally face six weeks of bespoke work.
This boilerplate ships all nine of the primitives below, wired and tested.
1. Tamper-evident audit chain
Append-only, per-org, SHA-256-linked hash chain. See architecture/database for the four-layer tamper-evidence design (RLS + UPDATE trigger + DELETE trigger + WORM flag).
- Library:
src/libs/AuditChain.ts. - Schema: migration
0016-audit-chain. - CLI verifier:
npx tsx src/scripts/audit-verify.ts --org <slug>returnsPASS/FAILwith broken-row id. - Dashboard:
/dashboard/audit-chainshows the latest 200 entries with a "Verify chain" button. - Currently emitting from: Stripe webhooks, invite actions, account deletion, suppression events.
Use this for compliance-grade events (subscription upgraded, invite sent,
account deletion scheduled). The regular audit_log table stays for
operational events ("user signed in") at high volume.
2. Idempotency layer
Idempotency-Key HTTP contract for any mutating endpoint, with request-hash
mismatch detection.
- Same key + same request → cached response replayed (header
Idempotency-Replayed: true). - Same key + different request → 409 Conflict with
idempotency_key_request_mismatch. - New key → handler runs, response captured for 24h.
Library: src/libs/Idempotency.ts. Backing table: idempotency_keys
(migration 0019). Daily cron idempotency-cleanup purges expired rows.
Wired on GET /api/v1/me (universal opt-in) and POST /api/stripe/webhook
(keyed by Stripe-Event-Id).
3. Evidence completeness scoring
Generic red / amber / green bars for any audit/compliance entity. The
checklist is data-driven, not code:
- Items live in
evidence_checklist_items(org_id IS NULLfor system-wide defaults; org-scoped rows override). - Records live in
evidence_completeness_records, one per (entity, item). - Scoring is weight-based — required items count more.
- Component:
<EvidenceCompletenessBar />(server) +<EvidenceChecklistPanel />(client) with optimistic toggles.
Three sample checklists ship in devSeed.ts: chargeback (8 items), leakage
(6 items), compliance (10 items). Mounted on
/dashboard/activity/[id] as the working example.
4. Suppression + WORM
Pair of compliance primitives:
- Suppression — generic, reversible "noise reduction" for false positives,
accepted risk, duplicates, data quality, or other (with mandatory note).
Reusable primitives:
<SuppressFindingButton/>,<SuppressionReasonDialog/>,<SuppressedRowBadge/>. Owner/admin gated server-side. Revocation emits its own audit event — no record is ever destroyed. - WORM —
audit_chain.wormboolean flag. Whentrue, an additional Postgres trigger refuses DELETE on the row even for super-users.
Use WORM for legally-bound retention (RGPD export, account deletion, Stripe payment lifecycle, KYC checkpoints).
5. RGPD data export
"Download my data" at /dashboard/settings?tab=security. The flow:
- User clicks "Request export" →
data_exportsrow inserted (status pending). - jobs event
account/export.requestedtriggersexportUserData. - Job collects every user-scoped row (profile, memberships, audit_log, credits_ledger, payments, subscriptions, preferences).
- Gzips a JSON blob, uploads to
exports/{userId}/{exportId}.json.gzvia the Storage abstraction. - Marks the row
ready(24h TTL), emails a signed download link.
Schema: migration 0014-rgpd-and-deletion.
6. 30-day account deletion
User types email in dialog → users.scheduled_deletion_at = now() + 30 days.
A daily cron process-scheduled-deletions (3am UTC) hard-deletes every user
past the deadline. FK onDelete: cascade covers memberships, sessions,
credits ledger, payments, subscriptions, data exports.
Cancel-link grace: user signs in any time during the 30 days, clicks the
emailed cancel link /cancel-deletion/{token}, and deletion is reversed.
7. Status page (public + operator)
- Public —
/[locale]/status(no-auth,revalidate = 0). Per-component pills, ongoing incidents, recently resolved (last 14 days). - Operator —
/[locale]/dashboard/admin/status. Post incident, add component, resolve incident. - Probe — scheduled job
status.health-proberuns every 5 min, opens auto incidents covering failing components, auto-closes on next healthy probe.
Manual incidents (no [auto] prefix) are never mutated by the probe. Schema:
status_components, status_incidents.
8. Maintenance mode
Global flag. MAINTENANCE_MODE=1 → every route except an allow-list redirects
(307) to /maintenance (locale-aware). Allow-list:
/maintenance, /api/health, /_next, /_vercel, /static, /favicon.ico.
Optional env vars: MAINTENANCE_ETA, MAINTENANCE_STATUS_URL,
MAINTENANCE_CHANGELOG_URL — all read by the maintenance page.
9. Security headers + security.txt
Production hardening shipped in next.config.ts:
- CSP —
default-src 'self', allow-listedscript-srcfor PostHog,connect-srcfor Sentry/PostHog/Stripe,frame-srcfor Stripe Checkout,frame-ancestors 'none'. - HSTS —
max-age=63072000; includeSubDomains; preload. X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin.- Permissions-Policy — camera, microphone, geolocation off; payment scoped to self.
CSP is skipped in development so HMR + the Next.js dev overlay keep
working. Forks adding a new third-party host edit cspDirectives —
'unsafe-eval' is permanently banned.
/.well-known/security.txt is auto-generated from SECURITY_CONTACT_EMAIL
and SECURITY_POLICY_URL. The Expires field is computed at request time so
the file never goes stale.
What's defensible
Every claim above maps to a file path you can grep. There is no "compliance theater" — no hand-waving, no "we have a SOC 2 dashboard." The boilerplate ships the primitives a SOC 2 auditor expects, with the SQL, TypeScript, and tests to back them.
That's the whole pitch.