Permissions catalog
Source of truth
The authoritative permissions matrix lives at docs/PERMISSIONS.md.
Edit PERMISSIONS.md, not this file. This page is a scan-friendly snapshot for
internal staff and contributors who want to look up common permissions quickly.
The drift test (src/test/permissions.matrix.test.ts) parses PERMISSIONS.md and will
fail the build if code references a permission not declared there, or if a permission in
that file is never referenced in code. Any change to permission wiring must go through
the canonical matrix.
See also: Architecture → Permissions system, Architecture → RLS.
Quick lookup — top permissions by domain
The following is a read-only snapshot of the most-frequently-used permissions. For the
full matrix (roles, RLS policies, portal scoping, sectional splits) consult
PERMISSIONS.md.
Bookings
| Permission | Meaning |
|---|---|
bookings.view |
List/read bookings |
bookings.create |
Create booking (wizard, import) |
bookings.edit |
Modify booking, add passengers |
bookings.delete |
Soft/hard-delete booking |
Customers
| Permission | Meaning |
|---|---|
customers.view |
List/read customers |
customers.create |
Create customer |
customers.edit |
Edit profile, upload documents |
customers.delete |
Delete customer |
Groups
| Permission | Meaning |
|---|---|
groups.view |
List/read groups |
groups.create |
Create group |
groups.edit |
Reassign passengers, link flights, misc expenses |
groups.delete |
Soft-delete group |
group_pricing.edit |
Save changes on the Pricing tab (rate sheet + tax lines) |
group_invoices.view |
See invoices tab |
group_invoices.issue |
Transition DRAFT → ISSUED (posts revenue + tax journal) |
Finance
| Permission | Meaning |
|---|---|
finance.view |
Access Finance module / reports / vouchers |
finance.create |
Record payments, post journals |
finance.edit |
Edit finance config, reconcile, reverse |
finance.payments.record |
Post a receipt to the ledger |
finance.journals.approve |
Approve a manual journal |
finance.journals.approve_own |
Break-glass self-approval (super-admin only) |
finance.tds.deduct |
Apply TDS on a supplier payment |
Approvals
| Permission | Meaning |
|---|---|
approvals.view |
See the approvals queue |
approvals.approve |
Approve, reject, or send-back bookings |
Agents & suppliers
| Permission | Meaning |
|---|---|
agents.view |
Read B2B partners |
agents.create |
Add partner |
suppliers.create |
Add supplier |
suppliers.edit |
Edit / record transactions |
The one rule
Edit PERMISSIONS.md, not this page. Any new catalog entry, role
grant change, or sectional split must be made in the canonical matrix so the drift test
stays green.
Adding a new permission — 5-step checklist
Mirrors CLAUDE.md
and §9 of PERMISSIONS.md.
- Add a row to the relevant section in
PERMISSIONS.md§6 (page-by-page action matrix). - If the permission is new, add it to §4 (catalog) and §5 (role grants) in the same file.
- Write a seed migration under
supabase/migrations/with a timestamped filename (YYYYMMDDHHMMSS_description.sql). The migration must:- Insert the permission into the
Permissiontable. - Grant it to the correct roles in
RolePermission. - Be idempotent (
IF NOT EXISTS,INSERT … WHERE NOT EXISTS).
- Insert the permission into the
- Reference the permission in code:
- Frontend route —
<ProtectedRoute requiredPermissions={['x.y']}> - Frontend action —
<PermissionGate permission="x.y">around the button/control - Backend handler —
await requirePermission('x.y')at the top of the route handler insrc/lib/api.ts
- Frontend route —
- Run
npm test. The drift test (src/test/permissions.matrix.test.ts) fails the build if code references a permission not in the matrix, or vice versa.
When renaming or deleting a permission, update PERMISSIONS.md, every code reference,
and add a rename/drop migration — in the same PR.
No role-name shortcuts
CEO, GM, and IT_ADMIN are super-admins because every permission row is seeded for
them explicitly (see migration 20260416020000_remove_permission_bypass.sql). They are
not super-admins via a hardcoded if role === 'CEO' branch.
- Do not add
if (role === 'CEO') return trueanywhere in code or RLS. - Do not special-case super-admins in
auth_user_has_permission(). - If someone needs privileged access, grant the permission through a migration.
Revoking a specific row in the PermissionsMatrix admin UI actually restricts CEO/GM/ IT_ADMIN — there is no bypass underneath.
How enforcement works
Permissions are checked at three layers. See Architecture → Permissions system for details.
| Layer | Mechanism | Where |
|---|---|---|
| Route | <ProtectedRoute requiredPermissions={[…]}> |
src/App.tsx |
| UI action | <PermissionGate permission="…"> |
component trees |
| API | await requirePermission('…') |
src/lib/api.ts (authoritative) |
| Database | RLS policies using auth_user_has_permission('…') |
Booking, Customer, Agent, Supplier, Payment, JournalEntry, JournalLine, FinanceConfig |
The order of precedence inside auth_user_has_permission() is: explicit user-level
deny → explicit user-level allow → role grant → otherwise denied.