Contributing
These are the working conventions for the codebase, distilled from AGENTS.md at the repo root. Read
AGENTS.md in full before non-trivial changes — it is the standing source of truth; this page
summarises and links into the rest of these docs.
The non-negotiables
Hard rules. A change that violates one is rejected at review:
- Tenant isolation is absolute. Every tenant-scoped query goes through
withTenant(enforced by an ESLint rule); Postgres RLS is default-deny + FORCE on every tenant-owned table as the final safety net. Never bypasswithTenant, never add aUSING (true)policy, never derive ownership from a client-supplied parameter. - Idempotency on every external-facing write. Capture, imports, provider webhooks, and queue consumers must be safe to retry (idempotency key + unique constraint and/or a dedup window). Delivery is at-least-once (see Queues).
- Compliance is correct from day one. Suppression/blacklist is enforced on the send path; GDPR blanking/erasure and retention are auditable; deletion records survive PII purge.
- Respondent anonymity is structural. Aggregates carry no re-identifying identity; per-bucket breakdowns are suppressed below the anonymity threshold (default 5) at SQL level — the fake DB mirrors this (see Testing).
- Secrets never touch source or the browser. Worker Secrets / Secrets Store only; the frontend sees public values only (see Configuration).
- Greenfield — no migration. No legacy data/client migration, no back-compat shims.
Ports & adapters discipline
Database, Email, AI, Auth, Queues, and Object Store each sit behind a clean TypeScript port
(packages/ports) with one initial adapter (packages/adapters). No provider SDK type may appear
outside its adapter — domain and route code depend only on the port. The planned Azure moves
(Postgres, AI Foundry, email) must be a new adapter + composition-root change, never an app rewrite.
See The Ai port & Gemini adapter for the canonical example.
Stop and request human review for
Authentication/identity, RLS policies, compliance/GDPR logic, billing, AI prompt design (see Prompts), secret handling, infrastructure/IaC, and any new third-party dependency. Flag and wait — don't merge these unreviewed.
The gate before every commit
A task isn't done until the gate is green:
npm run typecheck # all workspaces
npm run lint # includes the withTenant ESLint rule
npm run build --workspace frontend
npm test # vitest run across api/ + packages/ + frontend/ (root vitest.config.ts)
Capture the real exit code — don't pipe to tail/grep, which masks it. CI runs the same gate on
Linux (plus npm ci and npm audit --audit-level=high) on every push and PR; an ephemeral-Neon RLS
suite is a planned addition, not yet wired. See QA & testing for the full picture.
Dependency changes
CI runs npm ci on Linux; a plain npm install on Windows can record a package-lock.json missing
cross-platform optional deps. Whenever you add/remove/upgrade a dependency in frontend/ or api/:
clean-reinstall (rm -rf node_modules package-lock.json && npm install), verify npm ci succeeds
against the regenerated lock, and commit package.json + package-lock.json together.
Commit as you go
Stage and commit at sensible points as you develop — do not wait until the end of a session. Good moments: a plan task completes (one task → one commit where practical), a migration applies cleanly, a feature verifies end-to-end, before a risky refactor, after a green test run.
- Imperative subject under ~70 chars; the body explains the why.
- Don't bundle unrelated changes, commit secrets/
.env/node_modules/build artefacts, use--no-verify, or force-push shared branches. - Run
git statusbefore and after committing.
Branch model
The primary branch is main. A develop integration branch is being introduced; once it
lands, day-to-day work happens on develop and is promoted to main by pull request (with branch
protection on main). Deployment is underway via Cloudflare Workers Builds (the repo is connected;
a push to main triggers install → build → deploy per deployable) — follow the runbook sequence rather
than deploying ad hoc (see Cloudflare topology).
Style
British spelling in code comments, UI copy, and docs. Use lucide-react for icons, Inter for type,
tabular-nums for metrics/tables. Build shared UI primitives once in frontend/src/components/ui/ and
reuse them. Be concise in chat; reserve detail for code, commit messages, and the source documents.
Mind the Tailwind v4 class-precedence trap (decompose shorthand padding into axis classes when you also
set pt-*/pl-* at a breakpoint) — AGENTS.md has the detail.