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.