Skip to main content

25. Hardening — guardrail gates + security review

Date: 2026-06-06

Status

Accepted

Context

Step 25 adds automated guardrails and records a security self-review of the build so far. (The multi-agent adversarial review the earlier steps used is currently blocked by an Anthropic monthly spend limit; this is the manual pass in its place.)

Decision

  • Guardrail gate (scripts/guardrails.mjs, pnpm guardrails): fails on either forbidden pattern under src/, with file:line:
    1. Wise funding call (fundTransfer, fundWithBalance, .fund(, /transfers/…/payments) — money movement is DRAFT-ONLY (ADR-0007); the owner funds in Wise. None exist today; the gate keeps it that way.
    2. Secret in a NEXT_PUBLIC_* var (…SECRET|SERVICE_ROLE|SERVICE_KEY|PRIVATE|PASSWORD) — those ship to the browser. (The public anon key is correctly not flagged.) Wired into lefthook pre-push alongside typecheck + tests. (No GitHub Actions workflow exists in this repo yet — adding the same pnpm guardrails && pnpm typecheck && pnpm test to CI is a one-step follow-up in docs/test-checklist.md.)
  • test script fixed to vitest run (was bare vitest = watch mode, which would hang CI/pre-push); test:watch added for local use.

Security posture (reviewed)

  • Tenancy: RLS on every table; the service-role client (which bypasses RLS) is used only in server actions/services after an explicit auth check, and those reads are company-scoped. SECURITY DEFINER RPCs (payroll close/discard, invoice create/void, check reconcile) are revoked from anon/authenticated and granted only to service_role; advisors clean apart from the account-level "leaked password protection" toggle.
  • Money: integer minor units throughout; pay/invoice/interest/reconcile engines are pure + golden-tested; owner-gating on every money/destructive action; atomic RPCs with row-lock / optimistic-concurrency guards; double-pay/double-invoice/double-reconcile guards.
  • Attribution: financial RPCs set app.actor_id so the audit log records the acting owner under the service role.
  • Data minimization: bank accounts stored last-4 only; check OCR omits routing/account numbers; magic-link tokens hashed, single-use, expiring; secrets only in env.
  • A11y: components follow the project guidelines — visible labels, role="alert"/<output> for feedback, status conveyed by text + color (never color alone), ≥44px primary touch targets, semantic tables in horizontal-scroll wrappers. A full keyboard/contrast/screen-reader audit remains a manual checklist item.

Consequences

  • A push that introduces a funding endpoint or a client-exposed secret now fails locally before it can land.
  • Remaining hardening is tracked in docs/test-checklist.md: the integration test lane (RLS per tenant, concurrency-safe numbering/close, magic-link single-use), the GitHub Actions gate, enabling Supabase leaked-password protection, and the manual a11y/security audit + staging dry-run.