nextjsboilerplate Docs
Compliance & Trust

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> returns PASS / FAIL with broken-row id.
  • Dashboard: /dashboard/audit-chain shows 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 NULL for 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.
  • WORMaudit_chain.worm boolean flag. When true, 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:

  1. User clicks "Request export" → data_exports row inserted (status pending).
  2. jobs event account/export.requested triggers exportUserData.
  3. Job collects every user-scoped row (profile, memberships, audit_log, credits_ledger, payments, subscriptions, preferences).
  4. Gzips a JSON blob, uploads to exports/{userId}/{exportId}.json.gz via the Storage abstraction.
  5. 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-probe runs 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:

  • CSPdefault-src 'self', allow-listed script-src for PostHog, connect-src for Sentry/PostHog/Stripe, frame-src for Stripe Checkout, frame-ancestors 'none'.
  • HSTSmax-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.

On this page