Tenant Scoping
How tenant isolation works across all platform services.
Overview
All platform data is tenant-scoped. Users see only data for tenants they belong to. Platform superadmins can view all tenants.
How Scoping Works
Frontend
- User signs in via Access Control IDP.
- Frontend resolves tenant from:
- User's assigned tenants (via
tenantIdsin JWT) - Selected tenant in UI (stored in localStorage per device)
- User's assigned tenants (via
- All API requests include
tenantIdquery param. - If no tenant selected, frontend blocks data loading until resolved.
Backend
- Backend extracts
tenantIdfrom:- Query param (
?tenantId=...) - User's JWT (
user.tenantIdoruser.tenantIds[0])
- Query param (
assertTenantScope()enforces:- Non-superadmin without tenant scope → 403 Forbidden
- Valid tenant scope → filters all queries by tenant
- Single-resource lookups (e.g.,
GET /users/:userId) verify the resource belongs to the scoped tenant → 404 if mismatch.
Superadmin "All Tenants" Mode
- Users with
platform.superadminrole can bypass tenant scoping. - UI shows "All Tenants" option in tenant selector.
- Backend allows unscoped queries for superadmins only.
- Auto-resets to scoped mode if user loses superadmin role.
Required Headers/Params
| Endpoint Type | Required Param | Notes |
|---|---|---|
| List/search | tenantId query param | Required unless superadmin |
| Single resource | tenantId query param | Recommended; enforced if provided |
| Mutations | tenantId in body or query | Required |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 403 "Tenant scope required" | No tenantId and not superadmin | Pass tenantId or assign user to tenant |
| 404 on valid resource ID | Resource belongs to different tenant | Check tenant assignment or use correct tenant |
| Empty results | Wrong tenant selected | Switch tenant in UI header |
| "All Tenants" not visible | User lacks platform.superadmin | Assign role if authorized |
Integration with Platform Services
Services that consume Access Control tenant scoping:
| Service | Tenant Source | Notes |
|---|---|---|
| VoicePro | JWT tenantIds + UI selector | Mapped via TenantRegistry.voiceProTenantId |
| TeeTime | JWT tenantId | Direct tenant ID from token |
| CRM | Slug-based (migrating to UUID) | See CRM tenant migration docs |
| Facilities | JWT tenantId | Direct tenant ID from token |
| SCL | JWT tenantId | Direct tenant ID from token |
Code References
- Tenant scope enforcement:
apps/access-control/access-control-backend/src/app/controllers/users-query.controller.ts assertTenantScope()helper: Reusable pattern for all query controllers- Superadmin check:
isPlatformSuperadmin()in controllers - TenantRegistry model:
libs/prisma/access-control-client/prisma/schema.prisma