Tenancy

i18n coverage dashboard

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

ConceptScope
TenantThe top-level isolation boundary; has its own users, roles, models, and files
UserBelongs to exactly one tenant; tokens encode tenantId
Model / page / commandAuthored at the tenant scope; the platform scope is a special "system" tenant
RecordCarries 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

LayerEnforcement
DatabaseEvery dynamic table has a tenant_id column; all queries filter on it
RepositoryDynamicDataMapper rejects queries without a tenantId parameter
Row policyComposable with row-level permissions (see Security Hardening)
CacheRedis keys are prefixed with aura:t:<tenantId>:
FileMinIO 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>
PatternExampleScope
Shared, tenant in payloadprod.shared.record.updatedDefault; tenant id is a payload field
Per-tenant topicprod.t.tenant_001.export.requestedOnly 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 list

A 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>/list returns 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.id on 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.

Related