Busflow Docs

Internal documentation portal

Skip to content

Backoffice Database Schema

This document details the concrete physical database schema for the Backoffice Bounded Context (schema: backoffice). The Backoffice schema serves as the root authority for business configuration, operational staff, abstract product definitions, third-party inventory, CRM, and all financial planning/quoting.

Physical Entity Relationship Diagram

mermaid
erDiagram
    OPERATOR ||--o{ USER_TENANT_ASSIGNMENT : "grants access"
    AUTH_USER ||--o{ USER_TENANT_ASSIGNMENT : "assigned to"
    AUTH_USER ||--o{ USER_ACCESS_GRANT : "receives"
    OPERATOR ||--o{ CREW_MEMBER : "employs"
    AUTH_USER ||--o| CREW_MEMBER : "linked to"
    OPERATOR ||--o{ VEHICLE : "owns"
    OPERATOR ||--o{ SUPPLIER : "contracts"
    OPERATOR ||--o{ RESELLER : "partners"
    OPERATOR ||--o{ PASSENGER_PROFILE : "manages"
    OPERATOR ||--o{ NOTIFICATION_TEMPLATE : "configures"
    OPERATOR ||--o{ COSTING_SHEET : "creates"
    OPERATOR ||--o{ TOUR_TEMPLATE : "defines"
    OPERATOR ||--o{ OPERATOR_INTEGRATIONS : "configures"
    OPERATOR ||--|| OPERATOR_SETTINGS : "configures"
    OPERATOR ||--o| TENANT_SUBSCRIPTION : "subscribes"

    SUPPLIER ||--o{ ALLOTMENT : "provides"
    COSTING_SHEET ||--o| TOUR_TEMPLATE : "baseline budget"
    COSTING_SHEET ||--o| TOUR_DEPARTURE : "instance budget"
    COSTING_SHEET ||--|| CHARTER_QUOTE : "custom budget"
    COSTING_SHEET ||--o{ PRICE_MATRIX : "generates"
    TOUR_TEMPLATE ||--o{ TOUR_DEPARTURE : "schedules"
    OPERATOR ||--o{ BOARDING_POINT_STOP : "maintains"
    OPERATOR ||--o{ DOOR_PICKUP_ZONE : "defines"
    TOUR_TEMPLATE ||--o{ TEMPLATE_BOARDING_POINT_ASSIGNMENT : "assigns stops"
    TOUR_TEMPLATE ||--o{ TEMPLATE_DOOR_PICKUP_ZONE_ASSIGNMENT : "enables zones"
    RESELLER ||--o{ CHARTER_QUOTE : "requests"

    CREW_MEMBER ||--o{ CREW_QUALIFICATION : "holds"
    CREW_MEMBER ||--o{ CREW_ABSENCE : "records"
    VEHICLE ||--o{ VEHICLE_INSPECTION : "schedules"
    OPERATOR ||--o{ IMPORT_JOB : "uploads"
    IMPORT_JOB ||--o{ IMPORT_ERROR_ROW : "captures failures"

Table Definitions

operators

The root tenant entity representing a bus tour company. All domain entities reference the operator via tenant_id. Lifecycle: ONBOARDINGACTIVESUSPENDEDCHURNED.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique operator identifier
nameVARCHARNot NullCompany trading name
legal_nameVARCHARNot NullRegistered legal company name (for invoices)
addressVARCHARRegistered business address
countryVARCHARNot NullISO 3166-1 alpha-2 country code
default_localeVARCHARNot Null, Default: de-DEDefault language/locale for the tenant
default_currencyVARCHARNot Null, Default: EURISO 4217 currency code
vat_idVARCHAREU VAT identification number (USt-IdNr.)
tax_idVARCHARNational tax identification number
statusVARCHARNot Null, Default: ACTIVEONBOARDING (post-signup, pre-first-login), ACTIVE, SUSPENDED (non-payment or manager action), CHURNED (cancellation or GDPR deletion)
legal_formVARCHARForeign Key (legal_forms.id), NullableReference to a localized legal form (e.g., GmbH, UG, Einzelunternehmen). A global reference table stores legal forms with localized labels. Affects invoice legal text and DATEV mapping.
brand_configJSONB[planned] — white-label theming: { logo_url, primary_color, accent_color }. Extended shape TBD during booking widget implementation.
bank_detailsJSONB[planned] — SEPA payout data: { iban, bic, account_holder }. Not PCI-scoped. Volume-level AES-256 encryption sufficient. Consumed by DATEV export.
subscription_tierVARCHARNot Null, Default: CORE[planned] — Denormalized from tenant_subscriptions.plan_id. The Nhost custom claims webhook reads this column during JWT issuance to inject X-Hasura-Plan. Authoritative source: tenant_subscriptions. Tier values TBD by product.
deposit_configJSONBNullableOperator-level default deposit configuration: { percentage: number, type: 'PERCENTAGE' | 'FIXED', min_amount: number | null }. Overridable per TourTemplate. See PRODUCT_mollie-integration.md §3 for the cascading resolution logic.
final_payment_configJSONBNullableOperator-level final payment timing: { reminder_days_before_start: number, escalation_days_before_start: number, flag_days_before_start: number }. Overridable per TourTemplate. See PRODUCT_mollie-integration.md §4.2.
cancellation_policyJSONBNullableTenant-level default cancellation fee schedule (CancellationPolicy VO): { tiers: [{ days_before_start: number, fee_percentage: number }], minimum_fee: number | null, currency: string }. Overridable per TourTemplate. See cancellation-protocol.md §CancellationPolicy for resolution cascade and runtime calculation.
ticket_issuance_triggerVARCHARDefault: DEPOSIT_PAIDDEPOSIT_PAID or FULLY_PAID. Controls when the system issues tickets for this operator's bookings. Overridable per TourTemplate. See ADR-014.
created_atTIMESTAMPDefault: now()Record creation timestamp
updated_atTIMESTAMPDefault: now()Record last update timestamp

operator_integrations

Third-party integration status tracking per tenant. Replaces single-purpose provider columns with a generic, extensible registry pattern. Seeded during ProvisionTenant (see ADR-003). The UI reads status to gate provider-dependent features (e.g., publishing TourDepartures to Commerce requires Mollie CONNECTED).

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique integration record
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
integration_typeVARCHARNot NullIntegration provider: MOLLIE, DPA, DATEV, META_WHATSAPP, AWS_SES, AWS_SNS (extensible enum)
statusVARCHARNot Null, Default: PENDINGPENDING (provisioned, awaiting connection), CONNECTED (active and functional), FAILED (connection failed after retry exhaustion), DISCONNECTED (manually disabled or revoked)
external_idVARCHARNullableExternal system identifier (e.g., Mollie merchant/organization ID)
configJSONBNullableIntegration-specific configuration. Shape varies by integration_type — see typed interfaces below.
error_messageTEXTNullableLast error message (populated when status transitions to FAILED)
connected_atTIMESTAMPTZNullableWhen status transitioned to CONNECTED
created_atTIMESTAMPTZDefault: now()Record creation
updated_atTIMESTAMPTZDefault: now()Last update

IMPORTANT

Unique constraint: (tenant_id, integration_type) — one record per integration type per tenant.

WARNING

Subscription Tier Gating: Premium B2B integrations (DATEV, META_WHATSAPP, AWS_SES) require a PRO or ENTERPRISE plan. The system enforces this during Row-Level capability checks (X-Hasura-Plan). See L3-6.2.2 Schema Matrix Enforcement for rules.

Known config Shapes

MOLLIE — see PRODUCT_mollie-integration.md §2.1:

typescript
interface MollieIntegrationConfig {
  organization_id: string;           // Mollie sub-account ID
  onboarding_status: 'PENDING' | 'COMPLETED' | 'NEEDS_DATA';
}

DATEV:

typescript
interface DATEVIntegrationConfig {
  consultant_number: string;         // Beraternummer
  client_number: string;             // Mandantennummer
}

DPA:

typescript
interface DPAIntegrationConfig {
  document_url: string;              // Signed DPA document storage URL
  signed_at: string;                 // ISO 8601 timestamp
}

META_WHATSAPP — see channel-provisioning-protocol.md §9:

typescript
interface MetaWhatsAppIntegrationConfig {
  waba_id: string;                   // Meta WhatsApp Business Account ID
  phone_number_display: string;      // Formatted phone number for UI display
  business_verification_status: 'NOT_VERIFIED' | 'PENDING' | 'VERIFIED';
}

AWS_SES — see channel-provisioning-protocol.md §9:

typescript
interface AwsSesIntegrationConfig {
  domain: string;                    // Verified sending domain
  sender_email: string;              // Primary sender email address
  dkim_status: 'PENDING' | 'SUCCESS' | 'FAILED';
}

AWS_SNS — see channel-provisioning-protocol.md §9:

typescript
interface AwsSnsIntegrationConfig {
  sender_id: string;                 // Registered Sender ID
  sender_id_type: 'PLATFORM' | 'OPERATOR'; // Phase 1: always PLATFORM
}

operator_settings

Tenant-scoped operational configuration. One row per operator — created during tenant provisioning. Settings referenced across bounded contexts (Operations, Commerce) read this table via soft FK. Separating settings from the operators table provides updated_at tracking per config change and avoids bloating the root entity with JSONB config columns.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique settings identifier
tenant_idUUIDForeign Key (operators.id), Unique, Not NullOwning tenant (1:1 with operator)
driver_cash_refund_enabledBOOLEANNot Null, Default: trueWhether drivers can perform cash refunds on onboard sales
driver_cash_refund_limitDECIMALNullableMaximum amount a driver can refund (null = unlimited). Default: 50.00.
onboard_payment_link_ttl_minutesINTNot Null, Default: 60TTL for onboard payment link CheckoutSessions (min: 15, max: 180). Longer than the standard 30 min — bus passengers may not check their phone immediately.
auto_refund_orphaned_onboard_paymentBOOLEANNot Null, Default: trueWhen a passenger pays for a voided OnboardSale (race condition), auto-initiate Mollie refund. If false, dispatcher reviews manually via dispatch board alert (OnboardPaymentOrphaned event).
created_atTIMESTAMPTZDefault: now()Record creation
updated_atTIMESTAMPTZDefault: now()Last update

NOTE

Provisioning: Seeded with defaults during ProvisionTenant (see ADR-003). Operators customize via the Backoffice workspace settings UI.

tenant_subscriptions

Billing plan state per operator. One row per tenant — created during ProvisionTenant with plan_id = CORE. The Nhost custom claims webhook queries this table to inject X-Hasura-Plan into the JWT session variables, enabling Hasura-level feature gating on premium tables and Actions.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique subscription identifier
tenant_idUUIDForeign Key (operators.id), Unique, Not Null1:1 with operator
plan_idVARCHARNot Null, Default: CORETier values TBD by product (placeholder: CORE, PRO, ENTERPRISE)
statusVARCHARNot Null, Default: ACTIVEACTIVE, PAST_DUE
payment_provider_sub_idVARCHARNullableExternal billing system reference (e.g., Stripe subscription ID)
created_atTIMESTAMPTZDefault: now()Record creation
updated_atTIMESTAMPTZDefault: now()Last update

NOTE

Provisioning: Seeded with plan_id = CORE, status = ACTIVE during ProvisionTenant (see ADR-003). Plan changes update both this table and the denormalized operators.subscription_tier column atomically.

tenant_scrub_logs

GDPR compliance proof for automated PII scrubbing. The pg_cron pipeline inserts one row per scrubbed entity to maintain an auditable ledger (as defined in L3-5.2.1), recording the UUID without storing any redacted PII. Retention: indefinite (compliance audit trail).

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique log entry
tenant_idUUIDForeign Key (operators.id), Not NullScrubbed tenant
entity_tableVARCHARNot NullTarget entity table (e.g., commerce.passengers, commerce.invoices)
entity_idUUIDNot NullID of the redacted entity
scrub_reasonVARCHARNot NullApplied deletion policy label (e.g., ttl_3y_last_booking, ttl_10y_gobd)
scrubbed_atTIMESTAMPTZNot Null, Default: now()Pipeline execution timestamp

NOTE

Scope mapping: change_events for operator_settings and tenant_subscriptions use scope = CONFIG.

User Identity (Nhost auth.users)

IMPORTANT

No separate users table exists in the Backoffice schema. Nhost Auth manages user identity and authentication entirely via the auth.users table. The Busflow system references auth.users(id) as a foreign key for all user-related relationships.

Nhost's auth.users provides: id (UUID), email, display_name, default_role (Hasura role), disabled (boolean — replaces the previous ACTIVE/DEACTIVATED lifecycle), created_at, last_seen. Nhost also manages password hashing, SSO providers, and email verification.

Platform vs. tenant roles:

  • Platform role (auth.users.default_role): user (regular) or busflow_staff (cross-tenant). Managed via Nhost Admin SDK.
  • Tenant role (user_tenant_assignments.default_role): MANAGER, DISPATCHER, DRIVER. Injected into JWT custom claims.
  • Capabilities (user_access_grants): Additive fine-grained grants. Resolved at application layer.

User deactivation: Handled via nhost.auth.manager.updateUser({ disabled: true }). Disabled users cannot log in. Active JWTs remain valid until expiry (max 15 min). For tenant-scoped removal, delete the user_tenant_assignment row — the user retains access to other tenants.

user_tenant_assignments

Many-to-many assignment of auth.users to operators (tenants). Each user has a default_role that determines their primary frontend access. In small operators, one person may fill multiple roles — admins grant additional capabilities via user_access_grants.

ColumnTypeConstraintsDescription
user_idUUIDPK, Foreign Key (auth.users(id))The assigned user (Nhost user ID)
tenant_idUUIDPK, Foreign Key (operators.id)The assigned tenant
default_roleVARCHARNot NullPrimary role: MANAGER, DISPATCHER, DRIVER. Determines x-hasura-default-role in JWT. [planned V1.1]: OWNER, VIEWER.
created_atTIMESTAMPDefault: now()Assignment creation timestamp
updated_atTIMESTAMPDefault: now()When the assignment was last modified

user_access_grants

Granular capability grants for users within a tenant. Enables small operators where one person fills multiple roles (e.g., an Manager who also dispatches and drives). Each grant adds a capability on top of the default_role.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique grant identifier
user_idUUIDForeign Key (auth.users(id)), Not NullThe user receiving the grant (Nhost user ID)
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant scope
capabilityVARCHARNot NullGranted capability: DISPATCH, FLEET_MGMT, BOOKING_MGMT, DRIVER_APP, CREW_MGMT, FINANCIAL_REPORTS.
granted_atTIMESTAMPDefault: now()Grant creation timestamp
granted_byUUIDForeign Key (auth.users(id)), NullableThe manager who created this grant (Nhost user ID)

NOTE

MANAGER implicitly has all capabilities. Access grants are additive — they never restrict, only extend what the default_role provides.

WARNING

RBAC × Hasura — Reevaluate During Implementation. The current model maps default_role (MANAGER, DISPATCHER, DRIVER) to separate Hasura roles with distinct permission sets. An alternative is a single TENANT_USER Hasura role with uniform tenant-scoped permissions, where the application layer handles capability differentiation (what a user can see/do) via user_access_grants. This simplifies Hasura metadata (one role config) but moves authorization complexity into NestJS. The right approach depends on Hasura's permission granularity for this use case and requires a decision during RBAC implementation.

crew_members

Operational staff record for drivers and guides. Linked to a users row for authentication (nullable — operators can register crew members before the system provisions their login). Lifecycle: ACTIVEINACTIVETERMINATED.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique crew member identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this crew member belongs to
user_idUUIDForeign Key (auth.users(id)), Unique, NullableLinked Nhost auth identity. Null if crew member registered but not yet given app access.
first_nameVARCHARNot NullFirst name
last_nameVARCHARNot NullLast name
roleVARCHARNot NullDRIVER, GUIDE, DRIVER_GUIDE — the crew member's operational capability
statusVARCHARNot Null, Default: ACTIVEACTIVE, INACTIVE, TERMINATED — only ACTIVE members appear in dispatch
phoneVARCHARContact phone number (used by dispatchers and Communications context)
emailVARCHARContact email (notification delivery)
license_numberVARCHARDriving license number (basic field; granular qualifications in crew_qualifications)
license_expiryDATEExpiration date of the driving license
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

vehicles

Physical fleet assets. Lifecycle: ACTIVEIN_MAINTENANCEDECOMMISSIONED.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique vehicle identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this vehicle belongs to
license_plateVARCHARUnique, Not NullVehicle license plate
modelVARCHARNot NullVehicle model (e.g., "Mercedes-Benz Tourismo")
vehicle_classVARCHARNot NullCOACH, MINIBUS, VAN, DOUBLE_DECKER — determines routing profile (height/weight restrictions)
statusVARCHARNot Null, Default: ACTIVEACTIVE, IN_MAINTENANCE, DECOMMISSIONED — only ACTIVE vehicles appear in dispatch
transmission_typeVARCHARNot Null, Default: MANUALMANUAL, AUTOMATIC — matched against crew qualification restrictions
capacityINTNot NullMaximum passenger capacity
current_mileage_kmINTCurrent odometer reading. Updated from telemetry sync or manual entry. Used for mileage-based maintenance intervals.
seat_map_layoutJSONBDynamic seat layout. Structure: { seats: [{ id: string, row: int, col: int, type: "STANDARD" | "PREMIUM" | "WHEELCHAIR", label: string, accessible: bool }] }. The id field must match SeatReservation.seat_identifier in Commerce.
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

suppliers

Third-party relationships essential for executing multi-day tours.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique supplier identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this supplier belongs to
nameVARCHARNot NullSupplier name
contact_emailVARCHARContact email address
service_typeVARCHARNot NullHOTEL, FERRY, GUIDE, etc.
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

allotments

Reserved inventory blocks (e.g., hotel rooms, ferry slots).

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique allotment identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this allotment belongs to
supplier_idUUIDForeign Key (suppliers.id), Not NullThe supplier providing the allotment
statusVARCHARNot Null, Default: ACTIVEACTIVE, CONSUMED, EXPIRED, CANCELLED
start_dateDATENot NullAllotment availability start
end_dateDATENot NullAllotment availability end
capacityINTNot NullAmount of inventory reserved
net_rateDECIMALNot NullNet price per unit
currencyVARCHARNot NullISO 4217 currency code (e.g., EUR, CZK)
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

resellers

Business clients (travel agencies, corporate accounts, tourism boards).

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique reseller identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this reseller belongs to
nameVARCHARNot NullReseller name
reseller_typeVARCHARNot NullAGENCY or CORPORATE
commission_rateDECIMALCommission percentage or amount
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

passenger_profiles

The central CRM record for a traveler.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique profile identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant managing this profile
emailVARCHARTraveler email
phoneVARCHARTraveler phone number
first_nameVARCHARNot NullFirst name
last_nameVARCHARNot NullLast name
date_of_birthDATERequired for demographic pricing (age brackets) and border manifests
dietary_needsJSONBStructured dietary requirements
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

boarding_point_library

Operator-level library of pickup locations. Each entry represents a physical stop that optionally also offers door pickup in its surrounding area. Stops belong to the operator and are reusable across templates via template_boarding_point_assignments. The 3-tier pickup model (Origin → Extra Stops → Door Pickup) is defined in boarding-points.md.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique library item
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
nameVARCHARNot NullDisplay name (e.g., "Dorfplatz Nachbardorf")
addressVARCHARFull address string
geo_coordinatesJSONB{ lat: number, lng: number } for map display, routing, and door pickup radius validation. Required when door_pickup_available = true (radius validation needs a center point).
zone_labelVARCHARNullableGrouping label (e.g., "Nachbardorf"). Provides implicit UI grouping — not a FK to a separate entity.
surchargeDECIMALNot Null, Default: 0Stop pickup surcharge in cents. €0 for stops that typically serve as the origin.
door_pickup_availableBOOLEANNot Null, Default: falseWhether this location offers door pickup in its surrounding area.
door_pickup_surchargeDECIMALNullableDoor pickup premium in cents (e.g., 1500 = €15). Standalone surcharge — not added on top of stop surcharge. Null if door_pickup_available = false.
door_pickup_radius_kmDECIMALNullableMax distance from stop's geo_coordinates for valid door pickup addresses. Null if door_pickup_available = false.
passenger_instructionsTEXTNullableTraveler-facing guidance (e.g., "South exit, look for BusFlow sign")
is_archivedBOOLEANNot Null, Default: falseSoft-delete. Archived items remain on existing template assignments. They disappear from pickers for new assignments.
created_atTIMESTAMPDefault: now()
updated_atTIMESTAMPDefault: now()

NOTE

Unique constraint: (tenant_id, name) — prevents duplicate stop names per tenant.

NOTE

No type enum. Whether a stop serves as the origin (free, main departure) or an extra stop (surcharge) is a per-template decision via is_origin on the template assignment. This allows multi-city operators to use different stops as the origin on different tours.

NOTE

No boarding_order. Boarding order and pickup times are operational concerns that depend on the finalized passenger list and actual door pickup bookings. Both are deferred to [v0.2] as a dispatch-side design. See boarding-points.md §Resolution Cascade.

NOTE

CHECK constraint: door_pickup_available = true OR (door_pickup_surcharge IS NULL AND door_pickup_radius_km IS NULL) — ensures door pickup fields are null when the feature is disabled.

template_boarding_point_assignments

Which boarding point library items are assigned to which tour template, with per-template configuration.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique assignment
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
tour_template_idUUIDForeign Key (tour_templates.id), Not NullTarget template
boarding_point_idUUIDForeign Key (boarding_point_library.id), Not NullAssigned library item
is_originBOOLEANNot Null, Default: falseMarks this stop as the departure origin for this template. Origin surcharge is always €0 regardless of overrides.
surcharge_overrideDECIMALNullablePer-template surcharge override. Null = use library default. Ignored if is_origin = true.
door_pickup_overrideBOOLEANNullableOverride door pickup availability for this template. Null = inherit from library.
door_pickup_surcharge_overrideDECIMALNullableOverride door pickup surcharge. Null = inherit from library.
display_orderINTNot Null, Default: 0Display sequence in the booking widget and template config. NOT the operational pickup route — that depends on actual bookings.
enabledBOOLEANNot Null, Default: trueQuick toggle to disable a stop for this template without removing the assignment.
created_atTIMESTAMPDefault: now()

NOTE

Unique constraint: (tour_template_id, boarding_point_id) — one assignment per library item per template.

NOTE

CHECK constraint: At most one is_origin = true per tour_template_id. Implemented as a partial unique index: UNIQUE(tour_template_id) WHERE is_origin = true.

ancillary_catalog_items

Operator-level library of bookable extras (insurance, room upgrades, excursions, meals). Reusable across templates via template_ancillary_assignments. The centralized-overridable cascade follows the same pattern as boarding points and deposit config. See ancillary-catalog.md for the full design.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique catalog item
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
typeVARCHARNot NullBehavioral category: INSURANCE, UPGRADE, EXTRA_LUGGAGE, EXCURSION, MEAL, OTHER. Determines refund rules, reporting bucket, and invoice line grouping.
labelVARCHARNot NullOperator-defined display name shown to passengers (e.g., "Reiserücktrittsversicherung")
descriptionTEXTNullableOptional longer description for the booking widget product page
cover_image_keyVARCHARNullableOptional image key for displaying the item in the booking widget
default_priceDECIMALNot NullDefault unit price in the operator's currency
currencyVARCHARNot Null, Default: EURISO 4217
is_per_passengerBOOLEANNot Null, Default: truetrue: price applies per passenger. false: price applies per booking.
max_quantityINTNullableMaximum units per booking. Null = unlimited.
tax_strategy_overrideVARCHARNullableSTANDARD_VAT or MARGIN_SCHEME_25. Null = inherit from the parent tour's tax strategy.
statusVARCHARNot Null, Default: ACTIVEACTIVE or ARCHIVED. Archived items remain on existing bookings but disappear from pickers for new assignments.
sort_orderINTNot Null, Default: 0Display order in the booking widget and template configuration
created_atTIMESTAMPDefault: now()
updated_atTIMESTAMPDefault: now()

NOTE

Unique constraint: (tenant_id, label) — prevents duplicate names within a tenant.

template_ancillary_assignments

Which ancillary catalog items are offered on which tour template, with optional price overrides. Follows the same assignment pattern as template_boarding_point_assignments.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique assignment
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
tour_template_idUUIDForeign Key (tour_templates.id), Not NullTarget template
ancillary_catalog_item_idUUIDForeign Key (ancillary_catalog_items.id), Not NullCatalog item being assigned
price_overrideDECIMALNullablePer-template price override. Null = use default_price from catalog.
included_by_defaultBOOLEANNot Null, Default: falsetrue: auto-added to every booking (e.g., travel insurance on premium tours). Passenger can opt out.
enabledBOOLEANNot Null, Default: trueQuick toggle to disable an extra for this template without removing the assignment.
created_atTIMESTAMPDefault: now()

NOTE

Unique constraint: (tour_template_id, ancillary_catalog_item_id) — one assignment per catalog item per template.

notification_templates

Operator-configurable message templates for automated communications.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique template identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this template belongs to
channelVARCHARNot NullEMAIL, WHATSAPP, PUSH
trigger_eventVARCHARNot NullSystem event that triggers the message
subjectVARCHARSubject line (if applicable)
body_templateTEXTNot NullTemplate content
localeVARCHARNot NullLanguage/locale
meta_template_nameVARCHARNullableMeta-approved HSM (Highly Structured Message) template identifier. Required for WHATSAPP channel — maps the Busflow-internal template to the Meta-approved version. Null for EMAIL/PUSH.
meta_template_namespaceVARCHARNullableMeta Business Account namespace. Required for WHATSAPP channel. Null for EMAIL/PUSH.

NOTE

Known trigger_event values: VEHICLE_SWAP_NOTIFICATION (consumed by VehicleSwapped — see vehicle-swap-protocol.md), QUALIFICATION_EXPIRING (consumed by QualificationExpiring), VEHICLE_INSPECTION_OVERDUE (consumed by VehicleInspectionOverdue), INCIDENT_BROADCAST (consumed by IncidentCreated — CRITICAL severity, any type: DELAY/BREAKDOWN/PASSENGER_ISSUE; see incident-broadcast-protocol.md), INCIDENT_ALLCLEAR (consumed by IncidentResolved — CRITICAL severity). For the full registry of all 22 trigger_event entries with channels, context variables, and dispatch modes, see notification-pipeline-protocol.md §7. The provisioning flow must seed these templates during tenant onboarding.

costing_sheets

The universal cost planning engine. Contains projected costs and tax strategy. Selling prices live on price_matrices — see costing-pricing-separation ADR.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique costing sheet identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this sheet belongs to
source_typeVARCHARNot NullTEMPLATE_BASELINE, DEPARTURE_CLONE, or CHARTER_CUSTOM
statusVARCHARNot Null, Default: DRAFTDRAFT, CALCULATED, LOCKED — LOCKED = costs frozen (not prices). Auto-locks when the system confirms the first booking.
versionINTNot Null, Default: 1Optimistic locking counter for concurrent access
parent_sheet_idUUIDForeign Key (costing_sheets.id), NullableFor DEPARTURE_CLONE: references the TEMPLATE_BASELINE the system cloned it from. Tracks revision lineage.
calculated_atTIMESTAMPTZNullableTimestamp of last successful cost calculation. Null if status = DRAFT.
fixed_costsJSONBDriver rates, depreciation, insurance
variable_costsJSONBDistance costs, tolls (charter sheets include leerkilometer_km, leerkilometer_cost, toll_segments[])
procurement_itemsJSONBTyped array of CostComponent
planned_contribution_marginDECIMALTarget contribution margin in €
break_even_paxINTMinimum passengers to break even
tax_strategyVARCHARNot NullSTANDARD_VAT or MARGIN_SCHEME_25
fx_configJSONBForeign exchange configurations
total_net_costDECIMALComputed net cost
currencyVARCHARDefault: EURBase currency
created_atTIMESTAMPDefault: now()Record creation timestamp
updated_atTIMESTAMPDefault: now()Record last update timestamp

price_matrices

Market-driven selling prices derived from a CostingSheet. Supports 1:N per CostingSheet (multi-channel pricing). Uses immutable append versioning — every mutation creates a new version. See costing-pricing-separation ADR and pricing-screen.md.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique price matrix identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this matrix belongs to
costing_sheet_idUUIDForeign Key (costing_sheets.id), Not NullSource cost calculation this price derives from
tour_departure_idUUIDForeign Key (tour_departures.id), NullableNull for template-level previews
channelVARCHARNot Null, Default: DEFAULTSales channel this matrix serves
statusVARCHARNot Null, Default: DRAFTDRAFT, PUBLISHED, ARCHIVED
versionINTNot Null, Default: 1Monotonic version counter
margin_configJSONBArray of MarginTarget value objects — margin rules applied to derive selling prices from costs
pricing_rules_snapshotJSONBNot NullDeep copy of PricingRule[] active at generation time. Immutable after creation. Enables historical audit of demographic discount rules.
pricing_config_snapshotJSONBNot NullDeep copy of PricingConfig (room surcharge, accommodation flag, season tiers, early-bird tiers) at generation time. Immutable after creation.
variantsJSONBNot NullTyped PriceVariant[] — resolved price variants including variable_cost_snapshot, applied_conditions[], and tax fields. See price-matrix-variant-spec.md for the full TypeScript interface.
list_priceDECIMALThe List Price — base adult, base-accommodation, default-season, no-early-bird gross selling price. All variants derive from this.
currencyVARCHARNot Null, Default: EURSelling price currency
generated_atTIMESTAMPTZNot NullWhen initially derived from CostingSheet
published_atTIMESTAMPTZNullableWhen made available to Commerce. Null if still DRAFT.
superseded_byUUIDForeign Key (price_matrices.id), NullablePoints to the next version in the immutable chain. Null = current version.
created_atTIMESTAMPTZDefault: now()Record creation timestamp

tour_templates

The abstract commercial product.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique template identifier
tenant_idUUIDForeign Key (operators.id), Not NullThe tenant this template belongs to
costing_sheet_idUUIDForeign Key (costing_sheets.id)Baseline budget
titleVARCHARNot NullTour title
descriptionTEXTTour description
tagsJSONBNullable, Default: '[]'Freeform operator-defined tags as a string array (e.g., ["Nordsee", "Städtereise", "Weihnachten"]). Supports filtering and booking widget categorization.
duration_daysINTNot NullNumber of days
statusVARCHARNot Null, Default: DRAFTDRAFT, ACTIVE, ARCHIVED
contentJSONBNullableRich tour content validated by TourContentSchema: { highlights: string[], included_services: string[], excluded_services: string[], travel_requirements: string[], itinerary: ItineraryDay[], cover_image_key: string?, gallery_image_keys: string[] }. Each ItineraryDay: { day_number, title, description?, locations: string[] }.
pricing_rulesJSONBArray of PricingRule input VOs: { demographic: string, age_min, age_max, discount_type, discount_value, label }. demographic is operator-defined (e.g., 'ADULT', 'CHILD', 'SENIOR', 'STUDENT'). Snapshotted on PriceMatrix at generation.
pricing_configJSONBNullablePricingConfig VO: { room_surcharge, includes_accommodation, season_config[], early_bird_config[] }. Null = no room/season/early-bird variants. Snapshotted on PriceMatrix at generation.
capacity_rulesJSONBArray of CapacityRule value objects: { strategy, max_capacity, overbooking_buffer_pct }
deposit_configJSONBNullablePer-template deposit override: { percentage: number, type: 'PERCENTAGE' | 'FIXED', min_amount: number | null }. Null = inherit from operators.deposit_config (tenant default). See PRODUCT_mollie-integration.md §3 for the cascading resolution logic.
final_payment_configJSONBNullablePer-template final payment timing override: { reminder_days_before_start: number, escalation_days_before_start: number, flag_days_before_start: number }. Null = inherit from operators.final_payment_config. See PRODUCT_mollie-integration.md §4.2.
cancellation_policyJSONBNullablePer-template cancellation fee schedule override (CancellationPolicy VO). Null = inherit from operators.cancellation_policy. Same schema as operator-level config. See cancellation-protocol.md §CancellationPolicy.
max_door_pickupsINTNullable, Default: 5Maximum door pickup bookings per departure. Null = unlimited. Travels via TripPublished → Commerce enforces at checkout. See boarding-points.md §Door Pickup Mechanics.
ai_embeddingVECTORpgvector extensionVector array for AI similarity
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

WARNING

Subscription Tier Gating: pricing_rules and pricing_config manipulation is reserved for PRO and ENTERPRISE tenants. Because Hasura column-level permissions do not evaluate session variables (X-Hasura-Plan), direct GraphQL update access on these columns is omitted. Operators must use the upsertPricingConfig Hasura Action, which guards capability at the NestJS layer. See ADR-030.

charter_quotes

B2B commercial wrapper for custom group requests.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique quote identifier
tenant_idUUIDForeign Key (operators.id)
reseller_idUUIDForeign Key (resellers.id)Partner requesting the quote
costing_sheet_idUUIDForeign Key (costing_sheets.id)The internal calculation supporting the quote
statusVARCHARDefault: DRAFTDRAFT, SENT, ACCEPTED, REJECTED
requested_dateDATENot NullDate of the requested trip

tour_departures

A concrete, scheduled departure of a TourTemplate. Represents a specific date-bound trip with all operational resources assigned. Publishing a TourDeparture creates the Commerce TourOffering via the TripPublished event.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique departure identifier
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
tour_template_idUUIDForeign Key (tour_templates.id), Not NullSource template
costing_sheet_idUUIDForeign Key (costing_sheets.id)Cloned instance budget (DEPARTURE_CLONE)
start_dateDATENot NullDeparture date
end_dateDATENot NullReturn date
statusVARCHARNot Null, Default: DRAFTDRAFT, READY, PUBLISHED, COMPLETED, CANCELLED
pricing_configJSONBNullablePer-departure override of template PricingConfig. Null = inherit from TourTemplate. Same schema as tour_templates.pricing_config.
departure_notesTEXTNullableFree-form per-departure notes (e.g., specific hotel name, route deviations). Template content remains the source of truth.
created_atTIMESTAMPDefault: now()Record creation timestamp
updated_atTIMESTAMPDefault: now()Record last update timestamp

change_events

Local audit trail for entities within the Backoffice schema. Uses a polymorphic entity reference (entity_type + entity_id) instead of per-entity nullable FK columns — see ADR-019. Hasura relationship resolution uses a computed field backed by a schema-local SQL function. The shared AuditTrailService (NestJS) writes all change_events, ensuring consistent old_values capture via SELECT ... FOR UPDATE and transactional co-location with the entity mutation.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique event
tenant_idUUIDForeign Key (operators.id), Not Null
user_idUUIDNullableSoft FK to auth.users(id). The actor — null for system/automation.
entity_typeVARCHARNot NullTarget entity type: operator, operator_settings, tenant_subscription, costing_sheet, tour_template, tour_departure, vehicle, supplier, allotment, crew_member, passenger_profile, reseller, notification_template, crew_qualification, crew_absence, vehicle_inspection, price_matrix, operator_integration, import_job
entity_idUUIDNot NullID of the mutated entity. Soft FK — no hard constraint (entity table varies by entity_type).
actionVARCHARNot NullINSERT, UPDATE, DELETE
scopeVARCHARNot Null, Default: GENERALGOBD, COMPLIANCE, DSGVO, CONFIG, GENERAL — canonical enum per ADR-019.
correlation_idUUIDNullableGroups related change_events across bounded contexts for cross-context saga tracing (e.g., vehicle swap writes to both Operations and Commerce).
old_valuesJSONBFull row snapshot before mutation. Captured via SELECT ... FOR UPDATE in the AuditTrailService. Null for CREATE.
new_valuesJSONBState after mutation. Null for DELETE.
created_atTIMESTAMPTZDefault: now()

NOTE

Index: (tenant_id, entity_type, entity_id) — covers all audit trail queries ("show history for entity X"). Additional partial index: (correlation_id) WHERE correlation_id IS NOT NULL for saga tracing.

NOTE

Scope mapping for this schema: CONFIG for operator, operator_settings, tenant_subscription, notification_template, operator_integration. DSGVO for passenger_profile. GENERAL for everything else. GOBD and COMPLIANCE are valid enum values but not typically used in the Backoffice context.

crew_qualifications

Granular qualification tracking for crew members. Extends the basic license_number/license_expiry on crew_members with typed, expiry-tracked professional certifications required for DACH bus operations.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique qualification identifier
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
crew_member_idUUIDForeign Key (crew_members.id), Not NullThe crew member holding this qualification
qualification_typeVARCHARNot NullLICENSE_D, LICENSE_D1, MODULE_95, ADR, FIRST_AID, PERSONENBEFOERDERUNGSSCHEIN, BORDER_VISA, DIGITAL_TACHOGRAPH_CARD
issued_dateDATEDate the authority granted the qualification
expiry_dateDATEExpiration date. Null = no expiry (e.g., certain first aid certs)
issuing_authorityVARCHARAuthority or institution that issued the qualification
restriction_notesTEXTFree-text restrictions for human context (e.g., "UK visa — valid until 2027-03"). Not used for programmatic checks — use restriction_type instead.
restriction_typeVARCHARNullableStructured restriction classification: AUTOMATIC_ONLY, GEOGRAPHIC, VEHICLE_CLASS, OTHER. Null = no restriction. Used by the dispatch availability engine to enforce conflict rules (e.g., AUTOMATIC_ONLY blocks assignment to manual-transmission vehicles).
statusVARCHARNot Null, Default: VALIDVALID, EXPIRING_SOON, EXPIRED, REVOKED. EXPIRING_SOON set by Hasura Scheduled Trigger — see workflow-orchestration.md.
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

NOTE

A Hasura Scheduled Trigger drives the EXPIRING_SOON status transition (check_qualification_expiry) running daily. It evaluates expiry_date - CURRENT_DATE <= threshold_days (configurable per tenant, default: 30). On transition, it emits a QualificationExpiring domain event consumed by Communications for manager notifications. See workflow-orchestration.md for the event trigger catalog.

Qualification Catalog

The canonical catalog of qualification types for DACH bus operations. All documents and implementations must reference this list.

TypeFull Name (DE)Expiry?Dispatch BlockValidation Rules
LICENSE_DFührerschein Klasse D✅ Yes🔴 Hard Blockexpiry_date NOT NULL. Required for all crew with role = DRIVER | DRIVER_GUIDE.
LICENSE_D1Führerschein Klasse D1✅ Yes🟡 Warningexpiry_date NOT NULL. Allows driving minibuses only (≤ 16 pax). Warning if dispatched to a COACH.
MODULE_95Berufskraftfahrerqualifikation (BKrFQG)✅ Yes (5 yr)🔴 Hard Blockexpiry_date NOT NULL. Mandatory for all commercial bus drivers in the EU. Renewal requires 35h training within 5 years.
PERSONENBEFOERDERUNGSSCHEINPersonenbeförderungsschein (PBefG §13)✅ Yes🔴 Hard Blockexpiry_date NOT NULL. Required by German Fahrlaubnis-Verordnung for fee-based passenger transport.
ADRADR-Bescheinigung✅ Yes (5 yr)🟡 WarningOnly relevant for routes carrying dangerous goods (rare in bus tourism). Warning if missing, not a dispatch block.
FIRST_AIDErste-Hilfe-Bescheinigung✅ / ❌🟡 Warningexpiry_date nullable — some certs are perpetual, others renewed. Warning if expired, not a hard block.
BORDER_VISAGrenzübertrittsgenehmigung / Visum✅ Yes🔴 ConditionalHard block only for routes crossing the relevant border. Uses restriction_notes to specify country/validity.
DIGITAL_TACHOGRAPH_CARDFahrerkarte (EU VO 165/2014)✅ Yes (5 yr)🟡 Warning / 🔴 Module-gatedexpiry_date NOT NULL. Mandatory for all commercial bus drivers. Separate from the license — the driver must physically carry it. Default: 🟡 Warning. Becomes 🔴 Hard Block when the operator enables the TACHOGRAPH integration (operator_integrations, Phase 2). Operators should not need to track this before opting into the Digital Tachograph module.

Phase 1 simplification: The schema defines all 8 types. The system defers BORDER_VISA conditional blocking (route-dependent) to Phase 2 — in Phase 1 it triggers a 🟡 Warning regardless of route. LICENSE_D1 resolution (D1 sufficient for vehicles with capacity ≤ 16 pax per EU Directive 2006/126/EC, Art 4) is also deferred to Phase 2 — Phase 1 requires LICENSE_D for all vehicles. DIGITAL_TACHOGRAPH_CARD dispatch blocking is module-gated: 🟡 Warning by default, 🔴 Hard Block only when the operator has enabled the TACHOGRAPH integration (Phase 2).

Transmission Restriction Matching

The restriction_type enum provides structured matching against vehicles.transmission_type:

VehicleCrew restriction_typeResult
MANUALNULL (no restriction)✅ OK
AUTOMATICNULL (no restriction)✅ OK
MANUALAUTOMATIC_ONLY🔴 BLOCKED
AUTOMATICAUTOMATIC_ONLY✅ OK

For CRUD operations (Create/Update/Delete/Revoke), dispatch validation algorithm (AssignCrew Action), and edge state handling, see crew-qualification-protocol.md.

crew_absences

Leave and absence tracking for crew members. Consumed by the dispatch board to block assignments during approved absences.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique absence identifier
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
crew_member_idUUIDForeign Key (crew_members.id), Not NullThe absent crew member
absence_typeVARCHARNot NullVACATION, SICK, REST_DAY, TRAINING, OTHER
start_dateDATENot NullAbsence start (inclusive)
end_dateDATENot NullAbsence end (inclusive)
statusVARCHARNot Null, Default: REQUESTEDREQUESTED, APPROVED, REJECTED. Only APPROVED absences block dispatch.
approved_byUUIDForeign Key (users.id), NullableThe manager/dispatcher who approved. Null while REQUESTED.
notesTEXTFree-text notes (e.g., doctor's note reference)
created_atTIMESTAMPDefault: now()Creation timestamp
updated_atTIMESTAMPDefault: now()Last update timestamp

vehicle_inspections

Scheduled regulatory inspection tracking for vehicles. Tracks deadlines for mandatory DACH bus inspections (HU, SP, tachograph calibration). Consumed by dispatch board to warn or block assignment of non-compliant vehicles.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique inspection identifier
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
vehicle_idUUIDForeign Key (vehicles.id), Not NullThe inspected vehicle
inspection_typeVARCHARNot NullHU (Hauptuntersuchung), SP (Sicherheitsprüfung), TACHOGRAPH_CALIBRATION, UVV (Unfallverhütungsvorschrift), EXHAUST_TEST (AU)
due_dateDATENot NullDeadline for this inspection
completed_dateDATEDate the workshop completed the inspection. Null = still pending/overdue.
statusVARCHARNot Null, Default: PENDINGPENDING, COMPLETED, OVERDUE. OVERDUE set by scheduled trigger when due_date < CURRENT_DATE AND completed_date IS NULL.
blocks_dispatchBOOLEANNot Null, Default: falseWhen true, dispatchers cannot assign the vehicle to any ServiceLeg. The system automatically sets this to true when status transitions to OVERDUE for safety-critical types (HU, SP).
notesTEXTInspector remarks or workshop reference

import_jobs

State tracking for the Legacy Data ETL pipeline. Uploads via the Magic Upload UI initiate a job.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique job identifier
tenant_idUUIDForeign Key (operators.id), Not NullOwning tenant
entity_typeVARCHARNot NullTarget structure: PASSENGER_PROFILES, TOUR_OFFERINGS, BOOKINGS, INVOICES
s3_keyVARCHARNot NullLocation of the uploaded blob
statusVARCHARNot Null, Default: PENDINGPENDING, PROCESSING, COMPLETED, COMPLETED_WITH_ERRORS, FAILED
bullmq_job_idVARCHARNullablePopulated via the incoming Hasura Event Webhook to track the background job
total_rowsINTDefault: 0Calculated after bounding download stream
imported_rowsINTDefault: 0Rows successfully processed/upserted
error_rowsINTDefault: 0Count of rows trapped by Dead-Letter Queue
created_atTIMESTAMPTZDefault: now()Record creation timestamp
completed_atTIMESTAMPTZNullableJob completion timestamp

import_error_rows

The Dead Letter Queue for the Magic ETL pipeline. Supports partial success for mass data migrations by catching single row constraint failures from GraphQL batch assertions without rolling back the entire batch.

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique error row index
import_job_idUUIDForeign Key (import_jobs.id), Not NullParent ETL job
row_indexINTNot NullOriginal CSV row number to display to the user
raw_payloadJSONBNot NullExact parsed representation of the bad row
error_messageTEXTNot NullHasura constraint or encoding evaluation failure reasoning
resolvedBOOLEANNot Null, Default: falseTrue when operator manually fixes the item in the UI

Internal documentation — Busflow