ADR-0002: Business → Outlet → Customer Multi-Tenant Hierarchy¶
Status: Accepted Date: 2026-05-03 Deciders: Architecture team Impacted Repos: All
Context¶
The platform serves multi-location operators (e.g., Malibu Jack's runs several FECs across Kentucky, Indiana, Texas). Each operator runs multiple locations under a single legal/financial umbrella. Customers may visit multiple locations within an operator's portfolio.
Without a fixed tenant model, repos invented their own scoping (some used
businessId, some outletId, some both, some neither), and permissions
ended up enforced inconsistently. Cross-tenant data leaks would be a
privacy and contractual disaster.
Decision¶
The platform operates on the following strict tenant hierarchy:
Business (top-level tenant)
└── Outlet (location)
├── Venue (sub-area)
│ └── Venue Resource (atomic bookable unit)
├── Service Item (catalog item)
├── Schedule
└── Booking
└── Customer (may belong to multiple Bookings across Outlets
within the same Business)
Tenant scope rules:
- Every business-domain query MUST filter by
business,outlet, or both. - Cross-
businessqueries are reserved for theSystem Adminrole. - The canonical role set is:
System Admin,Business Admin,Outlet Manager,Customer,Guest. - Default permission posture is deny-by-default: every BZ doctype
registers
permission_query_conditionsandhas_permissionhooks. - Multi-outlet managers have a session-resolved "active outlet" via
bookingzone.api.scope_context.resolve_manager_active_outlet. - Customer is scoped per-Business; the same person at two different Businesses is two distinct Customer records (unifying them across Businesses is explicitly out of scope to preserve tenant isolation).
Consequences¶
Positive¶
- Predictable scoping for every API surface.
- Permission audit becomes mechanical.
- Multi-outlet operators can grant per-outlet manager access without custom RBAC logic.
Negative / Trade-offs¶
- Adds query complexity (every query joins on tenant scope).
- Customer cannot have a single "global" identity across operators — this is a feature, not a bug, but breaks naive CRM unification.
Risks / Mitigations¶
- Risk: a developer forgets to scope a query, leaks cross-tenant data. Mitigation: deny-by-default permission hooks + integration tests that attempt cross-tenant access and assert refusal.
- Risk: scope-drift between repos.
Mitigation: tenant model enforced via published API contracts;
agents in any repo can verify scope by querying
bz-platform-context.
Alternatives Considered¶
- Flat single-tenant: everyone sees everything, scoped only by authentication. Rejected: incompatible with multi-tenant operator model.
- Account → Project hierarchy (Stripe-style): rejected as not matching the family-entertainment-center operator vocabulary; "Outlet" is the right unit for this domain.
- N-level deeply nested tenants: rejected as YAGNI.
Implementation Notes¶
- Reference implementation:
bookingzone.api.permissions.has_permission bookingzone.api.scope_context.- Frontend pattern: every nav menu item registers
requires.roleTypesand optionalaccessRightspredicates (seeadmin-web-app/client/src/lib/nav/registry.ts).
References¶
system-constitution.mdPrinciple IIglossary/domain-terms.md§ Tenant Hierarchy