Tenancy

AuraBoot is multi-tenant by design. A single deployment can host many tenants whose data, configuration, and credentials are kept apart. Tenancy is enforced at every layer of the platform, not bolted on after the fact.
Tenant model
| Concept | Scope |
|---|---|
| Tenant | The top-level isolation boundary; has its own users, roles, models, and files |
| User | Belongs to exactly one tenant; tokens encode tenantId |
| Model / page / command | Authored at the tenant scope; the platform scope is a special "system" tenant |
| Record | Carries tenant_id on every dynamic table |
Cross-tenant access is impossible by design. A request that resolves to tenant A cannot read or write tenant B's records, even via the dynamic query API.
When to use
- SaaS deployments hosting multiple customer organizations.
- A single organization that wants strong isolation between business units.
- Demo and trial environments that must not leak data into production tenants.
How it works
The platform resolves a MetaContext for every request from the JWT. MetaContext carries tenantId, userId, userPid, username, and roleIds; extension state (environment, member, and data-permission bypass flags such as environmentId, memberId) lives in separate ThreadLocals. All downstream code, including DynamicDataMapper, Kafka producers, and the file storage gateway, reads from MetaContext and refuses to operate without it.
Background jobs (Kafka consumers, scheduled tasks) construct a MetaContext explicitly using a system user id of 0L; failing to do so causes inserts into ab_data_change_log to violate NOT NULL constraints and roll back the transaction.
Data isolation
| Layer | Enforcement |
|---|---|
| Database | Every dynamic table has a tenant_id column; all queries filter on it |
| Repository | DynamicDataMapper rejects queries without a tenantId parameter |
| Row policy | Composable with row-level permissions (see Security Hardening) |
| Cache | Redis keys are prefixed with aura:t:<tenantId>: |
| File | MinIO object keys are prefixed with t/<tenantId>/... |
Model namespacing
Models are namespaced by tenant. The physical table name follows the pattern mt_<modelCode> and is shared across tenants, with isolation enforced by the tenant_id column. A tenant cannot enumerate or query models declared by another tenant. The system tenant owns platform-level tables such as the ab_audit_trail audit log.
File partitioning
auraboot-files/
t/tenant_001/
uploads/2026/05/28/<ulid>.pdf
exports/qualified-leads-2026-05.csv
t/tenant_002/
uploads/...A tenant's presigned URL is signed with a key that only resolves objects under its own prefix. A leaked URL cannot be used to read another tenant's files.
Credential scoping
Connector credentials (resolved through ConnectorCredentialResolver by cr_csp_connector_pid) are tenant-scoped. Tenant A's API key for an external CRM never enters tenant B's request context. The same cr_csp_connector_pid value used by different tenants resolves to different secrets.
Kafka topic naming
<env>.<tenant-or-shared>.<domain>.<event>| Pattern | Example | Scope |
|---|---|---|
| Shared, tenant in payload | prod.shared.record.updated | Default; tenant id is a payload field |
| Per-tenant topic | prod.t.tenant_001.export.requested | Only when ordering or throughput requires partitioning by tenant |
Prefer the shared-topic pattern. Per-tenant topics multiply broker resource cost; reserve them for tenants with workloads that justify the overhead.
Configuration
aura:
tenancy:
default-tenant: "tenant_default"
enforce-context: true # reject any request lacking a tenant
background-system-user-id: 0
storage:
file-prefix: "t/${tenantId}/"
cache:
key-prefix: "aura:t:${tenantId}:"
kafka:
topic-pattern: "${env}.shared.${domain}.${event}"
per-tenant-topics: [] # opt-in listA new tenant is created through the signed-in user's tenant-selection / registration flow: the frontend posts a TenantSelectionRequest to TenantSelectionController's /process endpoint, and the service layer TenantApplicationService.createTenantForUser(request, user) provisions the new tenant under the current user's identity (ultimately via TenantService.createTenant). The platform creates the tenant row and joins the user to the new tenant (creating the member and seeding the default roles).
Verify
- Issuing a token for tenant A and calling
/api/<pageKey>/listreturns zero rows from tenant B's data. - MinIO presigned URLs scoped to tenant A return 403 against tenant B's prefix.
- Background jobs log
tenant.idon every action; missing values cause a fail-fast error. - Removing a tenant deletes its records, files, and credentials and leaves other tenants untouched.
- Any per-tenant metric series stay bounded (at most one series per active tenant) and carry no unbounded label cardinality.