Permissions — Canonical Matrix
This file is the single source of truth for roles, permissions, and who-can-do-what. A drift test (
src/test/permissions.matrix.test.ts) parses this file and fails the build if the code uses a permission that isn't declared here, or if this file declares a permission no code references. If you add a page/section/action, update this file — the test will fail otherwise.
Last audited: 2026-04-16 (enterprise rebuild)
1. How to use this document
- Adding a new page, section, or action? Add a row to the relevant section in §6. If the action introduces a new permission name, add it to §4 (catalog) and §5 (role grants).
- Changing who can do what? Update §5. Update the seed migration (
supabase/migrations/*seed*permissions*.sql). Runnpm testto confirm the drift test still passes. - Removing a permission? Remove every reference from code first (grep
permission="x.y",requirePermission('x.y')), then remove from §4/§5, then add aDROPmigration.
2. Architecture
Permission enforcement happens at three layers:
- Route-level (client) —
<ProtectedRoute requiredPermissions={[...]}>insrc/App.tsx. Gates whole pages. - UI-level (client) —
<PermissionGate permission="...">wrapping individual buttons/actions in component trees. - API-level (server) —
requirePermission('...')insrc/lib/api.tshandler functions. The authoritative check. Also backed by Postgres RLS policies on sensitive tables (Booking,Customer,Agent,Supplier,Payment,JournalEntry,JournalLine,FinanceConfig) viaauth_user_has_permission(perm_name).
The SQL function order of precedence (post 20260416020000):
1. explicit user-level deny (UserPermission.allowed=false) → denied
2. explicit user-level allow (UserPermission.allowed=true) → granted
3. role grant (RolePermission) → granted
4. otherwise → denied
No role-name bypasses. CEO/GM/IT_ADMIN are super-admins by virtue of being seeded every permission row explicitly — not by a hardcoded short-circuit. Revoking a specific row in the PermissionsMatrix UI actually restricts them.
3. Role catalog
19 roles total — 4 admin-tier, 13 functional staff, 2 portal-scoped (partner/customer).
| Role | Tier | Purpose | Auto-grant? |
|---|---|---|---|
CEO |
super-admin | Chief executive; ultimate authority | Seeded every permission (20260416020000) |
GM |
super-admin | General Manager; operational head | Seeded every permission (20260416020000) |
IT_ADMIN |
super-admin | System administrator; tech owner | Seeded every permission (20260416020000) |
ADMIN_HR |
admin | HR/admin operations | Seeded most permissions (20260115000000 + 20260402210000 + 20260415210000 + 20260416010000) |
SALES_MANAGER |
functional | Sales team lead | Sales bundle |
SALES_EXEC |
functional | Sales representative | Sales-minimal bundle |
B2B_MANAGER |
functional | B2B/partner account manager | Partner + sales bundle |
B2B_EXEC |
functional | B2B sales rep | B2B-minimal bundle |
OPS_MANAGER |
functional | Operations lead (groups, inventory) | Operations bundle |
OPS_EXEC |
functional | Operations staff | Operations-minimal bundle |
FINANCE_MANAGER |
functional | Finance lead | Finance + approvals bundle |
ACCOUNTANT |
functional | Bookkeeper | Finance bundle |
CASHIER |
functional | Payments + receipts only | Cashier bundle |
TICKET_MANAGER |
functional | Ticketing lead | Tickets + reports |
VISA_OFFICER |
functional | Visa processing | Visa bundle |
AUDITOR |
functional | Read-only oversight | bookings.view, reports.export |
AGENT |
portal | B2B partner user (external) | Portal-only; no staff permissions |
CUSTOMER |
portal | End customer (external) | Portal-only; no staff permissions |
Custom roles can be created via the Admin → Permissions UI. They start with zero permissions and must be granted explicitly.
4. Permission catalog
Permission name format: <module>.<action> or <module>.<sub-resource>.<action>. Every permission here MUST be inserted into the Permission table via a seed migration, and MUST be referenced in code (otherwise the drift test fails).
4.1 Core staff modules
| Module | Permission | Meaning | Enforced at |
|---|---|---|---|
| agents (B2B partners) | agents.view |
List/read B2B partners | Route /agents, Sidebar |
agents.create |
Create new partner | <PermissionGate> on "Add Business Partner" |
|
| approvals | approvals.view |
See the approvals queue | Route /approvals, Sidebar |
approvals.approve |
Approve/reject/send-back bookings | <PermissionGate> in Approvals.tsx + server-side |
|
| bookings | bookings.view |
List/read bookings | Route /sales/bookings, /sales/bookings/:id |
bookings.create |
Create new booking (wizard, import) | Route /sales/bookings/new, API POST |
|
bookings.edit |
Modify booking fields, add passengers | <PermissionGate>, API PATCH |
|
bookings.delete |
Soft/hard-delete booking | <PermissionGate>, API DELETE |
|
| customers | customers.view |
List/read customers | Route /sales/customers, /people |
customers.create |
Create new customer | <PermissionGate>, wizard customer creation |
|
customers.edit |
Edit customer profile, documents | <PermissionGate>, API PATCH |
|
customers.delete |
Delete customer record | <PermissionGate>, API DELETE |
|
| finance | finance.view |
See Finance module / reports / vouchers | Route /finance |
finance.create |
Record payments, post journals | <PermissionGate> on "Add Payment", journal composer |
|
finance.edit |
Edit finance config, reconcile | RLS on FinanceConfig, Payment/Journal updates |
|
finance.payments.record |
Post a payment receipt to ledger | API POST /finance/vouchers/agent-receipt-allocate | |
| groups | groups.view |
List/read travel groups | Route /groups |
groups.create |
Create new group | <PermissionGate> on "Create Group" |
|
groups.edit |
Edit group, reassign passengers, link/unlink flights, misc expenses | <PermissionGate> + API PATCH/POST |
|
groups.delete |
Soft-delete group | API DELETE /groups/:id | |
| group_pricing | group_pricing.edit |
Edit the rate sheet on the group's Pricing tab (4 line items + tax lines) | API PATCH /groups/:id/pricing, <PermissionGate> on Pricing tab save |
| group_invoices | group_invoices.view |
See group invoices tab + individual invoices | Route (Invoices tab on group), API GET |
group_invoices.create |
Create a draft invoice from a group payer | <PermissionGate> on "Issue Invoice", API POST |
|
group_invoices.edit |
Edit a DRAFT invoice (line items, taxes, due date) | <PermissionGate> + API PATCH (server rejects on ISSUED) |
|
group_invoices.issue |
Transition DRAFT → ISSUED and post the revenue + tax journal | <PermissionGate> + API POST /group-invoices/:id/issue |
|
group_invoices.cancel |
Cancel an ISSUED invoice (posts reversing credit-note journal) | <PermissionGate> + API POST /group-invoices/:id/cancel |
|
| hotels | hotels.view |
List/read hotels | Route /hotels, Sidebar |
hotels.create |
Create hotel/facility | <PermissionGate> |
|
hotels.edit |
Update hotel fields, contracts | <PermissionGate> |
|
hotels.delete |
Remove hotel | <PermissionGate> |
|
hotels.export |
Export/print hotel reports | <PermissionGate> on export buttons |
|
| inventory | inventory.view |
Access inventory module (airline blocks, FIT, ground, B2B) | Route /inventory/*, /suppliers |
inventory.create |
Create airline block / FIT / ground transfer / B2B offer | <PermissionGate> |
|
inventory.edit |
Edit inventory record | <PermissionGate>, inline forms |
|
| leads | leads.view |
List/read leads | Route /sales/leads |
leads.edit |
Update lead status, edit fields | <PermissionGate> in detail dialog |
|
| partners | partners.create |
Create partner (alt to agents) | Legacy; kept for RLS policies |
partners.edit |
Edit partner | Legacy alias | |
partners.delete |
Delete partner | Legacy alias | |
| quotations | quotations.view |
List/read quotations | Route /sales/quotations |
quotations.create |
Create new quotation; also used for edit/send/delete until split | <PermissionGate> |
|
| reports | reports.view |
Access analytics / reports pages | Route /reports |
reports.export |
Download/print any report | <PermissionGate> on export buttons |
|
| requests | requests.view |
See service requests queue | Route /requests |
requests.create |
Create a service request (ops-facing; customer/agent portals are self-service) | requirePermission in API |
|
requests.edit |
Update or add notes to a service request | requirePermission in API |
|
| suppliers | suppliers.create |
Add supplier | <PermissionGate> |
suppliers.edit |
Edit supplier, record transactions | <PermissionGate> |
|
suppliers.delete |
Remove supplier | <PermissionGate> |
|
| tickets | tickets.view |
List/read tickets | Route /tickets |
tickets.edit |
Update passenger names, upload docs, issue | <PermissionGate> |
|
tickets.approve |
Approve exception request, issue ticket | <PermissionGate> |
|
| visa | visa.view |
List/read visa cases | Route /visa |
visa.create |
Create new visa case | <PermissionGate> |
|
visa.edit |
Update case status, upload/delete docs | <PermissionGate> |
4.2 Admin module
| Permission | Meaning | Enforced at |
|---|---|---|
admin.view |
Access /admin and /people/employees |
Route guards |
Legacy admin.edit kept for backwards-compat with existing RLS. New code should use the specific admin.* permissions defined in §4.4 below.
4.3 Cross-cutting (dashboard, chat)
| Permission | Meaning | Enforced at |
|---|---|---|
dashboard.view |
Show Dashboard nav item and KPIs | Sidebar nav filter |
chat.view |
Access internal chat | Route /internal/chat |
4.4 Granular sectional permissions (phase 1)
The coarse finance.view / admin.view gate the ROUTE. These sectional permissions gate individual sections inside those pages so an admin can e.g. hide Profit & Loss from an Accountant while still letting them see Trial Balance. A user needs BOTH the parent route permission AND the sectional permission.
Finance reports — each has independent view + export:
| Permission | Meaning |
|---|---|
finance.reports.trial_balance.view |
Trial Balance report |
finance.reports.trial_balance.export |
Export Trial Balance |
finance.reports.day_book.view |
Day Book report |
finance.reports.day_book.export |
Export Day Book |
finance.reports.balance_sheet.view |
Balance Sheet |
finance.reports.balance_sheet.export |
Export Balance Sheet |
finance.reports.profit_loss.view |
Profit & Loss |
finance.reports.profit_loss.export |
Export P&L |
finance.reports.group_profit_loss.view |
Group Profitability |
finance.reports.group_profit_loss.export |
Export Group P&L |
finance.reports.aging.view |
Aging Report |
finance.reports.aging.export |
Export Aging |
finance.reports.receivables_payables.view |
Receivables & Payables summary |
finance.reports.receivables_payables.export |
Export R&P |
finance.reports.gst_summary.view |
GST Summary |
finance.reports.gst_summary.export |
Export GST |
finance.reports.stock_status.view |
Stock Status (inventory valuation) |
finance.reports.stock_status.export |
Export Stock Status |
finance.reports.group_status.view |
Group Status |
finance.reports.group_status.export |
Export Group Status |
finance.reports.settlements.view |
Settlement Report |
finance.reports.settlements.export |
Export Settlements |
Finance — TDS (Tax Deducted at Source):
| Permission | Meaning |
|---|---|
finance.tds.view |
View TDS rate master and deduction ledger |
finance.tds.deduct |
Apply TDS on a supplier payment |
finance.tds.export |
Export the quarterly 26Q CSV |
Finance workflow — split from the coarse finance.create / approvals.approve:
| Permission | Meaning |
|---|---|
finance.payments.verify |
Verify a pending payment |
finance.payments.reject |
Reject a pending payment |
finance.payments.refund |
Issue a refund |
finance.journals.approve |
Approve a manual journal entry |
finance.journals.approve_own |
Override: bypass maker-checker to approve/reject a journal YOU created. Super-admin only |
finance.journals.reject |
Reject a manual journal entry |
finance.journals.bulk_approve |
Bulk approve journals |
finance.journals.reverse |
Post a reversal voucher |
finance.journals.reverse_own |
Override: bypass maker-checker to reverse a journal YOU created. Super-admin only |
finance.bookings.approve_finance |
Finance-tier booking approval |
finance.bookings.reject_finance |
Finance-tier booking rejection |
finance.cancellations.approve_airline |
Approve airline cancellation refund |
finance.cancellations.approve_b2b |
Approve B2B buyer cancellation |
finance.allocations.agent_receipt |
Allocate on-account agent receipt |
finance.allocations.supplier_advance |
Allocate supplier advance |
finance.stock_adjustment.post |
Post opening/closing stock adjustment |
finance.ledger.rebuild |
Rebuild ledger from source |
finance.config.edit |
Edit finance config (prefixes, GL defaults) |
Finance period/year management:
| Permission | Meaning |
|---|---|
finance.periods.create |
Create accounting period |
finance.periods.lock |
Lock accounting period |
finance.periods.unlock |
Unlock period |
finance.periods.close |
Permanently close period (exec-level) |
finance.years.create |
Create financial year |
finance.years.close |
Close financial year (exec-level) |
Admin — split from the monolithic admin.view:
| Permission | Meaning |
|---|---|
admin.dashboard.view |
Admin landing dashboard |
admin.audit.view |
Audit log access |
admin.audit.export |
Export audit log |
admin.currency.view |
View currencies |
admin.currency.create |
Create currency |
admin.currency.edit |
Edit currency |
admin.currency.delete |
Delete currency |
admin.cities.view |
View service cities |
admin.cities.create |
Create city |
admin.cities.edit |
Edit city |
admin.cities.delete |
Delete city |
admin.locations.view |
India locations hierarchy |
admin.integrations.view |
View email/WhatsApp/SMS config |
admin.integrations.edit |
Edit integrations |
admin.integrations.test |
Send test messages |
admin.config.edit |
System config (branding, numbering) |
admin.permissions.view |
View permissions matrix |
admin.permissions.edit |
Edit permissions matrix (privilege escalation — super-admin only) |
admin.users.view |
View staff users |
admin.users.create |
Create staff user |
admin.users.edit |
Edit staff user role |
admin.users.delete |
Delete staff user (super-admin only) |
admin.users.reset_password |
Reset staff password |
admin.roles.view |
View custom roles |
admin.roles.create |
Create custom role |
admin.roles.edit |
Edit custom role |
admin.roles.delete |
Delete custom role (super-admin only) |
Agents — split from coarse agents.*:
| Permission | Meaning |
|---|---|
agents.deactivate |
Soft-disable partner |
agents.credit_limit.set |
Modify credit limit |
agents.commission.set |
Modify commission rate |
agents.access_key.manage |
Create/reset partner portal login |
agents.ledger.view |
Partner ledger access |
agents.ledger.export |
Export partner ledger |
agents.allocate_receipt |
Allocate on-account receipt to bookings |
4.5 Portal-scoped permissions (NON-STAFF)
Portal users (role AGENT or CUSTOMER) authenticate into their own scoped APIs. Their data access is enforced by ownership checks in the handler (e.g. customerId === auth.uid()), not by permissions in the matrix. The staff permission matrix does NOT apply to portal users — they get zero staff permissions.
Portal users are gated only by allowedRoles on their routes:
- /customer/** → allowedRoles={['customer']}
- /partner/** → allowedRoles={['agent']}
Actions available inside portals are tracked in §6.10 (Customer Portal) and §6.11 (Partner Portal) but are NOT enforced against the matrix — only against ownership + role.
5. Role → permission grants
Canonical grants (as seeded by migrations through 20260416020000). Legend: ✓ = granted, - = not granted.
5.1 Super-admin & admin tiers
| Permission | CEO | GM | IT_ADMIN | ADMIN_HR |
|---|---|---|---|---|
| agents.* | ✓ | ✓ | ✓ | ✓ |
| approvals.* | ✓ | ✓ | ✓ | ✓ |
| bookings.* | ✓ | ✓ | ✓ | ✓ |
| customers.* | ✓ | ✓ | ✓ | ✓ |
| finance.* | ✓ | ✓ | ✓ | ✓ |
| groups.* | ✓ | ✓ | ✓ | ✓ |
| hotels.* | ✓ | ✓ | ✓ | ✓ |
| inventory.* | ✓ | ✓ | ✓ | ✓ |
| leads.* | ✓ | ✓ | ✓ | ✓ |
| partners.* | ✓ | ✓ | ✓ | ✓ |
| quotations.* | ✓ | ✓ | ✓ | ✓ |
| reports.* | ✓ | ✓ | ✓ | ✓ |
| requests.* | ✓ | ✓ | ✓ | ✓ |
| suppliers.* | ✓ | ✓ | ✓ | ✓ |
| tickets.* | ✓ | ✓ | ✓ | ✓ |
| visa.* | ✓ | ✓ | ✓ | ✓ |
| admin.* | ✓ | ✓ | ✓ | ✓ |
| chat.view | ✓ | ✓ | ✓ | ✓ |
| finance.journals.approve_own | ✓ | ✓ | ✓ | - |
| finance.journals.reverse_own | ✓ | ✓ | ✓ | - |
Maker-checker overrides.
finance.journals.approve_ownandfinance.journals.reverse_ownare break-glass permissions that let the holder approve/reject or reverse a journal entry they themselves created, bypassing the default segregation-of-duties rule. They are granted ONLY to the three super-admin roles (CEO/GM/IT_ADMIN). ADMIN_HR has every other finance permission but NOT these — HR is not an approver of record. No other functional role holds them;FINANCE_MANAGERorACCOUNTANTmust route their own journals to a different approver.
5.2 Functional staff (abbreviated — canonical bundles)
| Role | Grants |
|---|---|
SALES_MANAGER |
bookings.*, customers.create/edit/delete, quotations.create, leads.edit, groups.create, reports.export, group_pricing.edit, group_invoices.create, group_invoices.edit + bookings.view, customers.view, quotations.view, leads.view, group_invoices.view |
SALES_EXEC |
bookings.*, customers.create/edit, quotations.create, leads.edit + bookings.view, customers.view, quotations.view, leads.view, group_invoices.view |
B2B_MANAGER |
bookings., partners., agents.create, customers.create, quotations.create, reports.export, group_pricing.edit, group_invoices.create, group_invoices.edit + agents.view, bookings.view, customers.view, quotations.view, group_invoices.view |
B2B_EXEC |
bookings.*, partners.edit, customers.create, quotations.create + agents.view, bookings.view, customers.view, group_invoices.view |
OPS_MANAGER |
bookings.view, suppliers., hotels., inventory.*, groups.create, reports.export, approvals.approve + bookings.view, inventory.view, approvals.view, group_invoices.view |
OPS_EXEC |
bookings.view, suppliers.edit, hotels.edit, hotels.export, inventory.edit + bookings.view, inventory.view, group_invoices.view |
FINANCE_MANAGER |
finance.*, finance.payments.record, approvals.approve, reports.export, group_pricing.edit, group_invoices.create, group_invoices.edit, group_invoices.issue, group_invoices.cancel + finance.view, bookings.view, approvals.view, group_invoices.view |
ACCOUNTANT |
finance.*, finance.payments.record, reports.export, group_invoices.create, group_invoices.edit + finance.view, bookings.view, group_invoices.view |
CASHIER |
finance.create, finance.payments.record + finance.view, bookings.view |
TICKET_MANAGER |
tickets.edit, tickets.approve, reports.export + tickets.view, bookings.view |
VISA_OFFICER |
visa.create, visa.edit + visa.view, bookings.view |
AUDITOR |
reports.export + all *.view |
5.3 Portal roles
| Role | Staff-matrix grants |
|---|---|
AGENT |
none (portal-scoped via route allowedRoles + ownership) |
CUSTOMER |
none (portal-scoped via route allowedRoles + ownership) |
6. Page-by-page action matrix
This is the inventory of every user-triggerable action, the permission it requires, and where the enforcement lives. The drift test confirms that every permission string here actually appears in code, and vice versa.
6.1 Sales → Customers (/sales/customers)
| Section | Action | Permission | Enforcement |
|---|---|---|---|
| Route | View page | customers.view |
<ProtectedRoute> |
| Header | Sync customers | customers.edit |
(should gate — currently none) |
| Header | Export CSV | customers.view |
client-only |
| Header | Import CSV | customers.create |
<PermissionGate> recommended |
| Header | Create customer | customers.create |
<PermissionGate> |
| Row | Edit customer | customers.edit |
<PermissionGate> |
| Row | Delete customer | customers.delete |
<PermissionGate> |
| Profile | Upload doc to Drive | customers.edit |
server-side |
| Profile | Delete Drive doc | customers.edit |
server-side |
| Profile | View ledger | customers.view |
server-side scoping |
6.2 Sales → Leads (/sales/leads)
| Section | Action | Permission |
|---|---|---|
| Route | View page | leads.view |
| Header | Create lead | leads.edit |
| Detail | Update status | leads.edit |
| Detail | Convert to quotation | quotations.create |
| Detail | Convert to booking | bookings.create |
6.3 Sales → Quotations (/sales/quotations)
| Section | Action | Permission |
|---|---|---|
| Route | View page | quotations.view |
| Header | Create quotation | quotations.create |
| Row | Edit | quotations.create (treat as edit, currently same perm) |
| Row | Send to customer | quotations.create |
| Row | Delete | quotations.create (destructive — consider quotations.delete future) |
6.4 Sales → Bookings (/sales/bookings) + Booking Detail (/sales/bookings/:id) + Wizard (/sales/bookings/new)
| Section | Action | Permission |
|---|---|---|
| Route (list) | View page | bookings.view |
| Route (detail) | View page | bookings.view |
| Route (wizard) | Create new | bookings.create |
| List header | Create booking | bookings.create |
| List header | Import bookings (CSV/XLSX) | bookings.create |
| List row | Print invoice | bookings.view |
| Detail / List | Edit booking | bookings.edit |
| Detail / List | Add passengers | bookings.edit |
| Detail / List | Send message / WhatsApp | bookings.view |
| Detail / List | Add payment | finance.create |
| Detail / List | Request visa case | visa.create |
| Detail / List | Send back (to ops) | approvals.approve |
| Detail / List | Resubmit to finance | bookings.edit |
| Detail / List | Delete booking | bookings.delete |
| Detail | Customer ledger | customers.view |
6.5 Groups (/groups)
| Section | Action | Permission |
|---|---|---|
| Route | View page | groups.view |
| Header | Create group | groups.create |
| Card | Delete group | groups.delete |
| Passenger | Mark group leader | groups.edit |
| Passenger | Move to another group | groups.edit |
| Passenger | Drop from group | groups.edit |
| Flights tab | Link flight | groups.edit |
| Flights tab | Unlink flight | groups.edit |
| Hotels tab | Assign hotel | groups.edit |
| Ground tab | Assign transfer | groups.edit |
| Pricing tab | View rate sheet | groups.view |
| Pricing tab | Edit line items + tax lines (save) | group_pricing.edit |
| Pricing tab | Refresh rates from inventory | group_pricing.edit |
| Invoices tab | View payers + their invoices | group_invoices.view |
| Invoices tab | Create draft invoice for a payer | group_invoices.create |
| Invoices tab | Edit draft invoice | group_invoices.edit |
| Invoices tab | Issue draft → posts ledger | group_invoices.issue |
| Invoices tab | Create supplementary for issued invoice | group_invoices.create |
| Invoices tab | Cancel issued invoice (credit note) | group_invoices.cancel |
6.6 Approvals (/approvals) + Corrections (/corrections)
| Section | Action | Permission |
|---|---|---|
Route /approvals |
View queue | approvals.view |
Route /corrections |
View queue | approvals.view (currently bookings.view — drift; see §8) |
| Row | Approve booking | approvals.approve |
| Row | Reject booking | approvals.approve |
| Row | Send back for correction | approvals.approve |
| Row | Log communication | approvals.view |
| Row | Assign correction owner | approvals.approve |
| Row | Resubmit corrected | bookings.edit |
6.7 Visa (/visa, /visa/:id) + Tickets (/tickets, /tickets/:id)
| Section | Action | Permission |
|---|---|---|
Route /visa |
View pipeline | visa.view |
Route /visa/:id |
View case | visa.view |
| Header | New visa case | visa.create |
| Header | Upload document | visa.edit |
| Header | Sync visa cases | visa.edit |
| Row | Bulk status change | visa.edit |
| Row | Per-row status | visa.edit |
| Docs dialog | Delete visa doc | visa.edit |
Route /tickets |
View queue | tickets.view |
Route /tickets/:id |
View detail | tickets.view |
| Header | Sync tickets | tickets.edit |
| Header | Print report | tickets.view |
| Tab | Bulk update names | tickets.edit |
| Row | Update passenger name | tickets.edit |
| Row | Request exception | tickets.edit |
| Row | Approve exception | tickets.approve |
| Row | Issue ticket | tickets.approve |
| Detail | Upload ticket doc | tickets.edit |
6.8 Finance (/finance)
| Section | Action | Permission |
|---|---|---|
| Route | View module | finance.view |
| Accounts | Create GL account | finance.edit |
| Accounts | Edit GL account | finance.edit |
| Accounts | Delete GL account | finance.edit |
| Accounts | View ledger drill-down | finance.view |
| Payments | Verify payment | finance.create |
| Payments | Reject payment | finance.create |
| Payments | Record payment | finance.create |
| Payments | Refund payment | finance.edit |
| Vouchers | Post receipt / payment / contra | finance.create |
| Journal | Create manual journal | finance.create |
| Journal | Reverse journal | finance.edit |
| Journal | Approve manual journal | approvals.approve |
| Approvals | Approve booking (finance) | approvals.approve |
| Approvals | Reject booking (finance) | approvals.approve |
| Cancellations | Approve B2B cancellation | approvals.approve |
| Cancellations | Approve airline cancellation | approvals.approve |
| Allocate | Allocate agent receipt | finance.payments.record |
| Reports | View trial balance, P&L, balance sheet, day book, aging | finance.view |
| Reports | Export any | reports.export |
| Settings | Create/lock/close period | finance.edit |
| Settings | Rebuild ledger / quota ledger | finance.edit |
| Settings | Save finance config | finance.edit |
| Settings | Stock adjustment | finance.edit |
| TDS | View TDS rate master / deduction ledger | finance.tds.view |
| TDS | Export 26Q CSV | finance.tds.export |
6.9 Partners (admin) (/agents)
| Section | Action | Permission |
|---|---|---|
| Route | View partners | agents.view |
| Header | Add partner | agents.create |
| Row | Edit partner | agents.edit |
| Row | View ledger | agents.view |
| Ledger | Allocate receipt | finance.payments.record |
| Ledger | Print statement | reports.export |
| Row | Set/reset access key | agents.edit |
| Row | Deactivate / reactivate | agents.edit |
| Row | Delete partner | agents.delete |
6.10 Customer portal (/customer/**) — role-scoped
Not enforced against staff matrix. Access gated by allowedRoles={['customer']} + API ownership checks.
Actions available: view own bookings, request group interest, download own invoice, upload payment with receipt, view visa status, download visa docs, accept/reject quotations sent to them, create/respond to service requests, edit own profile, view own statement, manage own sessions.
6.11 Partner portal (/partner/**) — role-scoped
Not enforced against staff matrix. Access gated by allowedRoles={['agent']} + API ownership checks.
Actions available: dashboard KPIs, view own bookings, view own customers, view own quotations, create booking (partner wizard → always on_account), add passengers, view/sell seat inventory, view/create/edit/print invoices, submit group-booking requests, view own reports (bookings, revenue, customers, invoices), edit contact person on profile, manage own sessions.
6.12 Inventory (/inventory/**)
| Route | Action | Permission |
|---|---|---|
/inventory/quota |
View blocks | inventory.view |
| Create/edit/delete block | inventory.create / inventory.edit / inventory.delete |
|
| Manage airlines | inventory.edit |
|
| Link/unlink block to group | inventory.allocate |
|
| Sell seats to B2B | inventory.edit |
|
| Record supplier payment | finance.create |
|
/inventory/fit |
View | inventory.view |
| CRUD | inventory.create / .edit / .delete |
|
| Record FIT supplier payment | finance.create |
|
/inventory/b2b-flights |
View | inventory.view |
| Create offer | inventory.create |
|
| Close offer | inventory.edit |
|
/inventory/ground-transfers |
View | inventory.view |
| CRUD | inventory.create / .edit / .delete |
|
| Assign / remove | inventory.allocate |
6.13 Suppliers (/suppliers)
| Section | Action | Permission |
|---|---|---|
| Route | View page | inventory.view (semantically should be suppliers.view — tracked in §8) |
| Header | Add supplier | suppliers.create |
| Row / Dialog | Edit supplier | suppliers.edit |
| Dialog | Delete supplier | suppliers.delete |
| Ledger | Add transaction | suppliers.edit |
| Ledger | Edit transaction | suppliers.edit |
| Ledger | Delete transaction | suppliers.delete |
| Payment | Apply TDS on supplier payment | finance.tds.deduct |
6.14 Hotels (/hotels)
| Section | Action | Permission |
|---|---|---|
| Route | View page | hotels.view |
| Header | Create hotel | hotels.create |
| Row | Edit hotel | hotels.edit |
| Row | Delete hotel | hotels.delete |
| Any | Export | hotels.export |
6.15 People (/people, /people/employees)
| Section | Action | Permission |
|---|---|---|
Route /people |
View directory | customers.view |
Route /people/employees |
View staff | admin.view |
| User mgmt | Create user | admin.view (should tighten — admin.edit) |
| User mgmt | Reset password | admin.view (should tighten) |
| User mgmt | Change role | admin.view (should tighten) |
| User mgmt | Delete user | admin.view (should tighten — also needs confirm dialog) |
6.16 Admin (/admin) — role+permission double-gated
| Section | Action | Permission |
|---|---|---|
| Route | View admin | admin.view + allowedRoles=['admin'] |
| Permissions matrix | Edit role permissions | admin.edit |
| Permissions matrix | Edit user overrides | admin.edit |
| Currencies | CRUD | admin.edit |
| Cities | CRUD | admin.edit |
| Integrations | Edit email/WhatsApp/SMS config | admin.edit |
| Audit logs | View | admin.view |
6.17 Reports (/reports) + Requests (/requests) + Communications (/communications, /whatsapp) + Chat (/internal/chat)
| Route | Permission |
|---|---|
/reports |
reports.view |
/requests |
requests.view |
/communications |
customers.view (drift — should be dedicated communications.view future) |
/whatsapp |
customers.view (drift — same) |
/internal/chat |
chat.view |
7. Sidebar visibility
Nav items are filtered by hasPermission(). Parent visible if user has any child's permission.
| Nav label | Path | Permission |
|---|---|---|
| Dashboard | /app |
dashboard.view (route uses allowedRoles instead — drift) |
| Inventory → Airline Blocks / FIT / B2B / Ground | /inventory/* |
inventory.view |
| Inventory → Hotels & Food | /hotels |
hotels.view |
| Inventory → Visa | /visa |
visa.view |
| Sales → Leads | /sales/leads |
leads.view |
| Sales → Requests | /requests |
requests.view |
| Sales → Quotations | /sales/quotations |
quotations.view |
| Sales → Bookings | /sales/bookings |
bookings.view |
| Groups | /groups |
groups.view |
| Finance | /finance |
finance.view |
| Customers | /sales/customers |
customers.view |
| Business Partners | /agents |
agents.view |
| Suppliers | /suppliers |
inventory.view (drift — should be suppliers.view) |
| People | /people |
customers.view |
| People → Employees | /people/employees |
admin.view |
| Workflow → Ops Approval | /approvals |
approvals.view |
| Workflow → Corrections | /corrections |
bookings.view (drift — should be approvals.view) |
| Workflow → Finance Approval | /finance?tab=approvals |
finance.view |
| Workflow → Tickets | /tickets |
tickets.view |
| Workflow → WhatsApp | /whatsapp |
customers.view (drift) |
| Workflow → Communications | /communications |
customers.view (drift) |
| Workflow → Internal Chat | /internal/chat |
chat.view |
| Reports | /reports |
reports.view |
| Admin | /admin |
admin.view |
8. Known drift (as of this audit)
Items where code/matrix/seed don't fully agree. Each is tracked here; fixing them is incremental work.
chat.viewreferenced but unseeded. App.tsx:187 requires it, but no migration inserts it. Fixed in this PR bysupabase/migrations/20260416030000_seed_chat_and_dashboard_view.sql.dashboard.viewreferenced by Sidebar but no route guard uses it./approute relies onallowedRoles={['admin','staff']}instead. Seeded this PR; tightening the route guard is a follow-up./correctionsusesbookings.view. Semantically this is an approvals/ops action; should migrate to a dedicatedcorrections.view. Tracked for a follow-up PR — when wired, addcorrections.viewto §4./communicationsand/whatsappusecustomers.view. Should migrate to a dedicatedcommunications.view. Tracked; when wired, addcommunications.viewto §4./suppliersroute usesinventory.view. Should migrate tosuppliers.view. Tracked; when wired, add to §4.agents.edit/agents.delete/groups.delete/inventory.delete/inventory.allocate/quotations.deletenot yet wired. The API enforces these actions via RLS and role-level grants, but there are no<PermissionGate>wrappers in the UI so row-level buttons remain visible regardless of permission. Tightening UI gates is a follow-up per PR; when added to code, re-add to §4.admin.edit/admin.users.*/admin.config.edit/admin.audit.viewnot yet split. User management actions in<UserManagement>aren't separately gated; all live behindadmin.view. Split when refactoring admin surface.- Route-level
allowedRoles=['admin','staff']on/appbypasses the permission matrix for dashboard access. Migrate torequiredPermissions={['dashboard.view']}in a follow-up. - Partner & customer portals are role-gated only (
allowedRoles={['agent']}/['customer']). Actions inside portals are ownership-gated by the API, not matrix-gated. This is intentional (see §4.5) but means there is no single matrix row describing partner self-service capabilities. - Granular permissions (§4.4) are seeded but mostly not yet wired in code. Migration
20260416050000inserts all ~70 sectional permissions and grants the right role bundles (e.g.finance.reports.profit_loss.viewto FINANCE_MANAGER but not ACCOUNTANT). The PermissionsMatrix admin UI surfaces them so admins can already toggle per-role. Currently wired: finance report button filtering inFinance.tsx(the user's explicit Accountant-vs-P&L example works). Not yet wired: export-button gates, workflow-split checks (e.g.finance.payments.verifystill goes through coarsefinance.create), admin tab-level splits (user management still behindadmin.view), agent CRUD splits. TheALLOWED_DOC_ONLYlist insrc/test/permissions.matrix.test.tsenumerates exactly which permissions are seeded-but-unwired; each follow-up PR that wires a batch removes its entries from that list.
9. Governance (how this stays true)
When you add a page/section/action:
1. Add a row to §6 under the appropriate page.
2. If the permission is new, add it to §4 catalog and §5 grants matrix.
3. Add the corresponding row(s) to a new seed migration under supabase/migrations/.
4. Reference the permission in code:
- Frontend: <PermissionGate permission="x.y"> around the action
- Backend: await requirePermission('x.y') at the top of the handler
- Route: <ProtectedRoute requiredPermissions={['x.y']}> if gating a whole page
5. Run npm test — the drift test will catch missing seed rows, orphaned permissions, or code that references permissions not in this file.
When you rename a permission:
1. Update §4 and §5 in this file.
2. Update every code reference in the same PR (grep the old name).
3. Add a migration that renames in the DB (UPDATE "Permission" SET name = ...) AND updates RLS policies referencing the old name.
4. Run npm test.
When you delete a permission:
1. Remove every code reference first.
2. Remove from §4 and §5.
3. Add a migration that drops the RolePermission rows and then the Permission row.
4. Run npm test.
The drift test is in src/test/permissions.matrix.test.ts. It will fail the build if:
- Code uses a permission string not declared in this file
- This file declares a permission that no code references (after a grace list for migrations)
10. Canonical changelog of this matrix
| Date | Migration | Change |
|---|---|---|
| 2026-01-15 | 20260115000000 | Initial 22-permission seed + admin-tier grants |
| 2026-04-02 | 20260402210000 | Added groups.view/edit/delete |
| 2026-04-15 | 20260415200000 | Permission-aware RLS on Booking/Customer/Agent/Supplier/Payment/Journal*/FinanceConfig |
| 2026-04-15 | 20260415210000 | Added customers.edit/delete, partners.*, suppliers.*, finance.edit, admin.edit + functional subsets |
| 2026-04-15 | 20260415220000 | Seeded all 13 functional role bundles |
| 2026-04-16 | 20260416010000 | Added *.view family + finance.payments.record; broadcast to staff roles |
| 2026-04-16 | 20260416020000 | Removed CEO/GM/IT_ADMIN role-name bypass from auth_user_has_permission(); grant every permission to those roles explicitly |
| 2026-04-16 | 20260416030000 | Added chat.view and dashboard.view to close drift with code |