RBAC Matrix β
Status: β Active
Roles source: roles.md Β· ADR-005Last updated: 2026-04-17
This document defines who can do what across all Busflow entities. It is the source of truth for Hasura permission rules and NestJS @RequiresCapability() guards.
Design Principles β
- Tenant isolation first. Every query and mutation filters by
x-hasura-tenant-id. No role bypasses this (exceptbusflow_staff). - Three core roles.
MANAGER,DISPATCHER,DRIVER. Additional roles (OWNER,VIEWER) are deferred to V1.1. - Additive capabilities.
user_access_grantsextends a role β never restricts. MANAGER implicitly holds all capabilities. - Keep it simple. Start with coarse permissions. Refine column-level or row-level restrictions when a real use case demands it.
Notation β
| Symbol | Meaning |
|---|---|
| β | Full access (CRUD) |
| π | Read-only |
| βοΈ | Create + Read + Update (no delete) |
| π | No access |
| β‘ | Via Hasura Action only (business logic enforced server-side, no direct table mutation) |
| π§© | Requires additional capability grant |
Backoffice Schema β
Capability Gate = the capability required for a non-MANAGER role to access. If the role already has the capability as a base grant (per roles.md), no additional
user_access_grantsentry is needed. DISPATCHER base capabilities:DISPATCH,BOOKING_MGMT,FLEET_MGMT.
| Entity | MANAGER | DISPATCHER | DRIVER | Capability Gate | Notes |
|---|---|---|---|---|---|
operators | βοΈ | π | π | β | MANAGER edits company settings. No delete (lifecycle via status). |
operator_settings | βοΈ | π | π | β | 1:1 with operator. No delete. |
operator_integrations | βοΈ | π | π | β | Mollie, DATEV, DPA connections. No delete (lifecycle via status). |
tenant_subscriptions | βοΈ | π | π | β | Billing plan state. MANAGER read/update. busflow_staff provisions during onboarding. No delete (lifecycle via status). |
user_tenant_assignments | β | π | π | β | Only MANAGER manages user access. |
user_access_grants | β | π | π | β | Only MANAGER grants capabilities. |
tour_templates | β | βοΈ | π | DISPATCH | Base DISPATCHER capability. |
tour_departures | β | βοΈ | π | DISPATCH | Drivers see their assigned departures (row-scoped). |
boarding_points | β | βοΈ | π | DISPATCH | Part of departure setup. Drivers see stop info. |
costing_sheets | β | π | π | β | Dispatchers need read access for margin context when quoting. Full financial detail requires π§© FINANCIAL_REPORTS. |
price_matrices | β | π | π | β | Dispatchers need published prices for booking management. |
crew_members | β | π | π | CREW_MGMT | MANAGER CRUDs. Dispatchers read for assignments (π§© CREW_MGMT for write). |
crew_qualifications | β | π | π | CREW_MGMT | Dispatchers read for dispatch validation. |
crew_absences | β | βοΈ | π | CREW_MGMT | Dispatchers create/approve (π§© CREW_MGMT required for write). |
vehicles | β | βοΈ | π | FLEET_MGMT | Base DISPATCHER capability. Drivers see assigned vehicle. |
vehicle_inspections | β | βοΈ | π | FLEET_MGMT | Base DISPATCHER capability. |
suppliers | β | π | π | β | Hotel/ferry/guide partners. |
allotments | β | π | π | β | Inventory blocks. |
resellers | β | π | π | β | Agency/corporate partners. |
charter_quotes | β | βοΈ | π | DISPATCH | B2B quotes. |
passenger_profiles | β | βοΈ | π | BOOKING_MGMT | Base DISPATCHER capability. Drivers see manifest names (row-scoped). |
notification_templates | β | π | π | β | Template config is MANAGER-only. |
change_events | π | π | π | β | Audit trail. System-written. Drivers don't access Backoffice audits. |
Commerce Schema β
| Entity | MANAGER | DISPATCHER | DRIVER | Capability Gate | Notes |
|---|---|---|---|---|---|
tour_offerings | π | π | π | β | Created by system (on TourDeparturePublished). Read-only. |
bookings | β | βοΈ | π | BOOKING_MGMT | Dispatchers manage bookings. Drivers see passenger count. |
passengers | β | βοΈ | π | BOOKING_MGMT | Drivers see manifest names. |
tickets | π | π | π | β | System-issued. All roles read. |
seat_reservations | β | βοΈ | π | BOOKING_MGMT | Dispatchers reassign seats. |
ancillaries | β | βοΈ | π | BOOKING_MGMT | Add-ons per booking. |
checkout_sessions | π | π | π | β | System-managed payment flow. Read-only. |
payments | π | π | π | FINANCIAL_REPORTS | Financial data. |
invoices | π | π | π | FINANCIAL_REPORTS | System-generated. Read-only. |
financial_ledgers | π | π | π | FINANCIAL_REPORTS | Sensitive financial aggregation. π§© grant needed. |
tax_ledger_entries | π | π | π | FINANCIAL_REPORTS | Tax data. π§© grant needed. |
Operations Schema β
| Entity | MANAGER | DISPATCHER | DRIVER | Capability Gate | Notes |
|---|---|---|---|---|---|
service_legs | β | βοΈ | β‘ | DISPATCH | Drivers transition via Actions (startServiceLeg, completeServiceLeg). Dispatchers cancel via cancelServiceLeg. |
leg_assignments | β | βοΈ | π | DISPATCH | Dispatchers assign crew. Drivers see own assignments. |
boarding_events | π | π | β‘ | β | Drivers create via offline sync. Read-only for workspace users. |
incidents | βοΈ | βοΈ | β‘ | β | Drivers create (offline). Dispatchers manage (acknowledge, assign, resolve via takeOverIncident, resolveIncident). |
issue_reports | βοΈ | βοΈ | β‘ | β | Drivers create via createIssueReport. Dispatchers link/resolve via linkIssueReportToIncident, resolveIssueReport. |
incident_issue_reports | π | βοΈ | π | β | Join table. Created by Actions. |
onboard_sales | π | βοΈ | β‘ | β | Drivers create (offline sync). Dispatchers can void/refund. |
expense_receipts | π | βοΈ | β‘ | β | Drivers submit. Dispatchers verify/reject (verifyExpenseReceipt, rejectExpenseReceipt). |
telemetry_points | π | π | β‘ | β | Drivers push GPS data (POST /api/telemetry). System-managed. |
route_waypoints | π | π | π | β | System-created via TripPublished. ETA updated by system. |
crew_duty_logs | π | π | π | β | System-populated. Phase 2 for write access. |
change_events | π | π | π | β | Operations audit trail. System-written. |
Communications Schema β
| Entity | MANAGER | DISPATCHER | DRIVER | Capability Gate | Notes |
|---|---|---|---|---|---|
conversations | π | π | π | β | System-created for broadcast threads. |
messages | π | βοΈ | π | β | Dispatchers approve broadcasts, view message history. |
contacts | π | π | π | β | System-managed (passenger-facing). |
channel_accounts | β | π | π | β | WhatsApp/Email provider config. MANAGER-only. |
Public & Passenger Access β
These roles operate outside the tenant RBAC system. They don't use user_tenant_assignments or x-hasura-tenant-id.
anonymous (Hasura public role) β
The Hasura anonymous role serves unauthenticated frontends: the booking widget, landing page, and marketing site. The system scopes permissions by the operator's public slug or domain β no JWT required.
| Entity | Access | Scope | Frontend |
|---|---|---|---|
tour_offerings | π | WHERE status = 'PUBLISHED' AND operator matched by public slug | Booking widget β tour catalog |
price_matrices | π | WHERE status = 'PUBLISHED' AND linked to public tour_offering | Booking widget β price display |
boarding_points | π | WHERE linked to published tour_departure | Booking widget β pickup selector |
seat_reservations | π | Availability count only (no passenger data) | Booking widget β seat picker |
bookings | βοΈ | CREATE only (via Hasura Action createBooking) | Booking widget β checkout flow |
NOTE
Booking widget tenant resolution: The widget identifies the operator via a public slug in the URL (e.g., book.busflow.de/reisebuero-mueller). The server resolves slug β tenant_id and applies it as the implicit filter. No x-hasura-tenant-id in the JWT β anonymous requests don't have JWTs.
passenger [future] β
Passengers interact with booking-widget (pre-trip) and passenger portal (post-booking). Authentication is lightweight β email + booking code or magic link, not traditional login. The identity model (Nhost auth.users vs. standalone token service) is an open architectural decision β see roles.md Β§Passengers. A dedicated B2C auth specification will define token TTLs, flow diagrams, and Hasura role configuration.
Expected data scope (pending auth spec):
| Entity | Access | Scope |
|---|---|---|
bookings | π | Own booking(s) only |
passengers | π | Own passenger record only |
tickets | π | Own tickets (QR code, Wallet pass) |
boarding_points | π | Linked to own booking's departure |
Consumer ETA Tracking (token-secured, no auth) β
GET /api/track/:tracking_token β no Hasura role. This is a standalone NestJS endpoint secured by a short-lived JWT tracking token (not a Hasura session). Returns vehicle position + ETA. See schema-operations.md Β§Consumer ETA Tracking.
NOTE
Why not a Hasura role? The tracking endpoint serves data from multiple schemas (Operations telemetry_points + route_waypoints) to unauthenticated passengers via a signed token. It's simpler and more secure as a dedicated NestJS route than as a Hasura permission on the anonymous role β the token carries the specific service_leg_id + passenger_id scope, and the response is a computed aggregate, not a raw table read.
Hasura Actions β Role Matrix β
Actions enforce business logic server-side. The handler validates the requesting user's role and capabilities.
| Action | MANAGER | DISPATCHER | DRIVER | Notes |
|---|---|---|---|---|
provisionTenant | π | π | π | busflow_staff only |
startServiceLeg | π | π | β | Guard: active LegAssignment for requesting crew member |
completeServiceLeg | π | π | β | Guard: active LegAssignment |
cancelServiceLeg | β | β | π | Guard: MANAGER required if boarding events exist |
takeOverIncident | β | β | π | Dispatcher acknowledges + takes ownership |
resolveIncident | β | β | π | Any DISPATCHER/MANAGER (no assigned_to check β shift handoff) |
createIssueReport | β | π | β | Per schema: x-hasura-role β { DRIVER, MANAGER }. Offline-capable via sync. |
linkIssueReportToIncident | β | β | π | Dispatcher links report β incident |
resolveIssueReport | β | β | π | Dispatcher resolves (OPEN only; IN_PROGRESS auto-resolves via inspection) |
verifyExpenseReceipt | β | β | π | Dispatcher confirms OCR output |
rejectExpenseReceipt | β | β | π | Dispatcher rejects with mandatory reason |
swapVehicle | β | β | π | Guard: requires FLEET_MGMT capability (DISPATCHER base) |
upsertPricingConfig | β | β‘ | π | Guard: requires premium X-Hasura-Plan (tier values TBD by product). Used to update pricing_rules on tour_templates. |
Driver Data Scoping β
Drivers access a narrow slice of data, scoped to their assignments:
Driver sees:
tour_departures WHERE id IN (leg_assignments.tour_departure_id WHERE crew_member_id = me)
service_legs WHERE id IN (my leg_assignments)
boarding_events WHERE service_leg_id IN (my legs)
passengers WHERE booking.tour_offering_id IN (my tour_offerings)
boarding_points WHERE tour_departure_id IN (my departures)
vehicles WHERE id IN (my leg_assignments.vehicle_id)NOTE
Hasura row-level permissions implement this scoping. The x-hasura-user-id session variable resolves the driver's crew_member_id via crew_members.user_id, then filters through leg_assignments. This avoids exposing any data from other drivers' assignments or other tenants.
Platform Role: busflow_staff β
| Access | Scope |
|---|---|
| All tenant data | Cross-tenant SELECT (no tenant_id filter) |
provisionTenant Action | Creates operators + seeds config |
| User management | Via Nhost Admin SDK |
| Audit trail | All mutations logged with user_id + correlation_id |
WARNING
No direct table mutations. Busflow staff mutate via Actions only (enforced by Hasura permission rules). All staff actions generate change_events with scope = CONFIG.
Implementation Notes β
Hasura role strategy: Start with 3 Hasura roles (
manager,dispatcher,driver) mapping directly touser_tenant_assignments.default_role. Evaluate Hasura inherited roles during implementation if capability-based granularity is needed at the Hasura layer (see ADR-005 Β§RBAC Warning).Capability resolution: Happens at the NestJS application layer via
@RequiresCapability()decorator. Hasura permissions handle role + tenant scoping; NestJS handles fine-grained capability checks on Actions.Column-level restrictions (deferred): Phase 1 uses table-level permissions. We can add column-level restrictions (e.g., hiding
bank_detailsJSONB from DISPATCHER) when a concrete privacy requirement surfaces.Row-level for Drivers: Driver permissions use relationship-based filtering (through
leg_assignmentsβservice_legsβ etc.). This is the most complex Hasura permission configuration β test thoroughly with multi-driver, multi-departure scenarios.Subscription Tier Gating: The Nhost custom claims webhook (the same mechanism that injects
x-hasura-tenant-idandx-hasura-roleper ADR-005) also queriestenant_subscriptions.plan_idand injectsX-Hasura-Planinto session variables. Premium features require explicit gating rules (tier-to-feature mapping TBD by product). The gating mechanism and strategies (including theupsertPricingConfigAction workaround for column-level limitations) are defined by ADR-030 and L3-6.2.2 (Schema Matrix Enforcement).