Skip to main content

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:

  1. 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 bypass withTenant, never add a USING (true) policy, never derive ownership from a client-supplied parameter.
  2. 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).
  3. 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.
  4. 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).
  5. Secrets never touch source or the browser. Worker Secrets / Secrets Store only; the frontend sees public values only (see Configuration).
  6. 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 status before 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.