Skip to content

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:

  1. Every business-domain query MUST filter by business, outlet, or both.
  2. Cross-business queries are reserved for the System Admin role.
  3. The canonical role set is: System Admin, Business Admin, Outlet Manager, Customer, Guest.
  4. Default permission posture is deny-by-default: every BZ doctype registers permission_query_conditions and has_permission hooks.
  5. Multi-outlet managers have a session-resolved "active outlet" via bookingzone.api.scope_context.resolve_manager_active_outlet.
  6. 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.roleTypes and optional accessRights predicates (see admin-web-app/client/src/lib/nav/registry.ts).

References

  • system-constitution.md Principle II
  • glossary/domain-terms.md § Tenant Hierarchy