Vantage Docs
Self-hosting

Tenant isolation

How is each tenant isolated at every hop?

A tenant is a workspace, which is a Zitadel organisation. Its organisation id is the tenant_id at every layer, and it is always taken from something the server verified, never from the request body of a user-facing call.

Per hop

HopWhere the tenant comes fromWhat enforces itCode
Sign-inurn:zitadel:iam:user:resourceowner:id in the id token (OIDC_TENANT_CLAIM)Zitadel organisation; the server keeps the session and gives the browser an encrypted __Host-sid cookieapps/web/src/server/auth/
Control plane (Postgres)withTenant() sets app.tenant_id for the transactionEvery tenant table has FORCE ROW LEVEL SECURITY and a tenant_isolation policy on tenant_id; an unset setting matches no rows. The server checks at start-up that every declared table has row security and exits if notdb/rls.sql, db/client.ts
Semantic layer (Cube)A JWT the server mints for the session's workspace, signed with CUBEJS_API_SECRETcheckAuth verifies it; queryRewrite strips any *.tenant_id filter the caller sent and adds <cube>.tenant_id = <tenant> for every cube the query touches, and rejects a query that touches none; schema and pre-aggregations are partitioned by app id tenant_<id>data/cube/cube.js
Cube to StarRocksCube's security contextCube connects as the workspace's ro_<slug> user, SELECT on its own database only; cube_svc is the fallback before that user existscube.js, /api/internal/cube/creds
SQL editorThe session, plus a project the user may run SQL inRuns as p_<project id>, granted SELECT table by table on that project's datasets and the tenant-wide ones; only SELECT and WITH statements; per-workspace hourly budget in Valkeyserver/sql.ts, dataplane readonly-grants
Warehouse dataThe workspace slugOne database per workspace, tenant_<slug>; every row also carries tenant_id and _source_id set by the dataplaneservices/dataplane/src/ddl.ts, load.ts
BYO dbtThe sessiondbt_<slug> is granted only on tenant_<slug>dbt-admin.ts
File uploadTicket from getUploadTicket, HS256, 5 minutes, after a write check on the sourceThe dataplane loads only into the ticket's workspace and sourceserver/upload.ts, dataplane auth.ts
MCPvmcp_ token → its workspace and userA token_lookup policy lets the lookup read only the row whose hash it presents; then normal row security as that userai/mcp-tokens.ts
Object storageThe workspace idStaged files under uploads/<tenant id>/<source id>/, in a bucket the svc-dataplane account alone can reachstage.ts
Valkey, VaultNORIMS_ENV and the slugKeys prefixed by environment; tenant secrets under secret/tenants/[<env>/]<slug>/sql.ts, secrets/vault.ts

Roles inside a workspace

Server-side only; the UI hiding a control is presentation.

VocabularyValuesGates
Workspace role (users.role)owner, admin, memberWorkspace settings, members, tenant-wide sources
Scope role (project_members, dashboard_grants)admin, editor, viewerWhich project and dashboard surfaces a user reaches; the action-to-role matrix is server/auth/permissions.ts

Deliberate cross-tenant readers

ReaderSeesWhy
internal_reader policies (app.internal_reader)The tenant list, due dashboards, stale ingestion runsCube's scheduled refresh, scheduled publishing, run sweeps
backup roleEvery row, SELECT onlyThe nightly pg_dump --enable-row-security; a table without a backup_reader policy stops the server at start-up
Service tokensAny workspace named in the bodyInternal callers of the dataplane and analytics-web; see below

What does not isolate by tenant

CredentialReachKeep it
INGEST_TOKENS_<NAME>Every dataplane /api/internal/* endpoint, for any tenantId or database it names, including creating warehouse users and tearing a workspace downinside the cluster; never with a customer
UPLOAD_TOKEN_SECRETSigns upload tickets for any workspacein Vault, on analytics-web and dataplane only
CUBE_SERVICE_TOKENAny workspace's schema and read-only warehouse credential through /api/internal/cube/* on the app hostCube and analytics-web only
CUBEJS_API_SECRETMints Cube tokens for any workspaceCube and analytics-web only
dataplane_svc, cube_svcAll warehouse databasesin Vault

Preview and prod share the StarRocks cluster and these two warehouse users. What keeps preview off prod's data is naming (preview_tenant_<slug>, preview_-prefixed users) and the application scoping every name by NORIMS_ENV, not a grant.

Cube's pre-aggregations in CubeStore are not removed when a workspace is torn down; the StarRocks database, its users and its staged files are.

Next

On this page