Skip to content

FAQ

Short, direct answers to questions developers and operators frequently ask. For the long version, follow the links.


How do I add a new permission?

Follow the 5-step checklist on Permissions catalog: add a row to docs/PERMISSIONS.md §4/§5/§6, write an idempotent seed migration under supabase/migrations/, reference the permission in code (<ProtectedRoute>, <PermissionGate>, requirePermission), and run npm test. The drift test will fail the build if any step is skipped.


How do I add a new API route?

Add the handler to src/lib/api.ts — the single handler monolith. At the top of any write route (POST/PATCH/DELETE) call await requirePermission('x.y') before doing any work. If a double-click could fire the same request twice, add an idempotency guard (see ops-approve / finance-approve for the pattern). New routes must have a corresponding test under src/services/*.test.ts. See API → Overview.


How do I add a new page with permission gating?

Wrap the route in src/App.tsx with <ProtectedRoute requiredPermissions={['x.y']}>. Inside the page, wrap individual action buttons with <PermissionGate permission="x.y"> so users who can view the page but not act on it still get a sensible UI. The page's API handlers must also call requirePermission — don't rely on the UI alone. See Architecture → Frontend routing.


Where do I add a migration?

Create a timestamped file under supabase/migrations/ with the format YYYYMMDDHHMMSS_description.sql. Migrations must be idempotent (IF NOT EXISTS, CREATE OR REPLACE, INSERT … WHERE NOT EXISTS). Apply locally with supabase db push. See Operations → Database migrations.


How does the app determine the current user's role?

On sign-in, Supabase returns a JWT. The server reads the user's row from the User table (joined to Role) and uses that role when evaluating auth_user_has_permission(). The client mirrors the role into its auth context for UI-layer gating, but the server check is authoritative. See Architecture → Permissions system.


How does RLS interact with requirePermission?

They are two independent layers that both must pass. requirePermission runs in the API handler and 403s the request early if the user lacks the permission. RLS runs at the database and will still silently hide rows (or reject writes) even if the handler is buggy. In practice you want both: requirePermission for fast, explicit errors and RLS as a safety net. See Architecture → RLS.


What's the difference between maker-checker and period-lock?

Maker-checker is a segregation-of-duties rule: the user who creates a journal cannot approve it. Period-lock is a time-window rule: no one can post to a closed accounting period regardless of role. Maker-checker is enforced in the approval handler; period-lock is enforced by a database trigger on JournalLine. See Features → Finance → Journals.


How do I run tests locally?

npm test runs the full suite (570+ tests). npx tsc --noEmit must also be clean before a PR is ready. For a single file: npx vitest run src/pages/finance/Finance.test.tsx. See Operations → Testing.


How do I preview the docs?

pip install -r docs/requirements.txt && mkdocs serve from the repo root. Open http://127.0.0.1:8000. Edits reload live.


Can partners see finance data?

No. The partner portal (/partner/**) is gated by allowedRoles={['agent']} and API ownership checks — partners can only see their own bookings, invoices, and ledger. Finance module routes (/finance) require finance.view, which no portal role holds. RLS on JournalEntry, JournalLine, and FinanceConfig enforces the same restriction at the database layer. See PERMISSIONS.md §4.5 and §6.11.


Why a single api.ts instead of multiple files?

Uniformity. Every handler goes through the same auth check, permission check, and transaction pattern. Splitting handlers across files historically produced drift — routes forgetting requirePermission, inconsistent error shapes, duplicated idempotency guards. The monolith is searchable, and the one-file-to-grep rule makes audits trivial. See API → Overview.


What's the release cadence?

main is the production branch. Squash-merged PRs cut a new semantic version via chore(release): x.y.z [skip ci] commits. Patch releases ship as soon as a fix merges; minor releases bundle features across a week or two. See Operations → CI/CD.


How do I trigger a preview deploy for a branch?

Push the branch to GitHub — Cloudflare Pages automatically builds a preview on every push. The preview URL appears on the PR as a check. Previews use the staging Supabase project; do not test against production data. See Integrations → Cloudflare Pages.