Skip to main content
Version: 1.7.0

Multi-Tenancy

Runs unlicensed

Available with no license, in the Development posture. A license adds the MCP and A2A planes, production rights and support — not this feature.

DVARA is a shared, multi-tenant AI governance platform. Every workspace runs on the same fleet of data-plane and control-plane nodes. Isolation is logical — scoped by workspace on every request — not physical. There is no schema per workspace, no pod per workspace, and no runtime sharding. This page explains what is isolated, what is shared, and how configuration composes.

Three levels: account, team, workspace

The model is not flat. Since 1.7.0 there are three levels, and each exists because something has to key on it:

levelwhat it iswhat keys on it
Accountthe customerquota bands and billing
Teama group of people inside that customeruser membership and roles
Workspacean isolated governance boundaryAPI keys, policies, budgets, credentials, audit rows — every workspace-scoped record

An account has many teams; a team has many workspaces. A workspace is still where isolation happens — every workspace-scoped record carries its workspace, and every query filters on it — so nothing about the isolation model changed. What changed is what sits above a workspace.

Users belong to a team, not to a workspace

A user's role is granted on their team and spans that team's workspaces. That is the part a flat reading gets wrong: you do not add someone to a workspace, you add them to a team, and they then have that role across the workspaces the team holds.

Quotas roll up to the account

A quota band belongs to the account, not the workspace. This is the reason the level exists at all: with bands on workspaces, one customer with five workspaces would get five allowances, and the meter would measure nothing useful.

A workspace is the unit you switch between

The Console and the Portal carry a workspace switcher, and views are workspace-scoped: you are looking at one workspace's keys, spend and audit at a time. The config bundle is sectioned the same way, so a data-plane pod receives per-workspace config rather than one undifferentiated document.

What this means when reading the rest of this page

A workspace still has a name, a region, a status (ACTIVE / SUSPENDED) and a metadata map that tunes governance for that workspace alone (see Per-workspace configuration). Everything below about resolution, scoping and RBAC is workspace-level and unchanged — the account and team sit above it.

How a request is resolved to a workspace

Every data-plane request carries an Authorization: Bearer <key> header. The DVARA API key is an opaque random token — not a JWT, no claims, no parseable payload. The server stores only a SHA-256 hash of the key; the plaintext is shown once at creation and never again. Each key belongs to exactly one workspace, and hash collisions are rejected at the database, so the same token cannot exist as two separate keys.

When a request arrives:

  1. The gateway hashes the token and looks it up in a distributed cache shared across every node — cache hits resolve in sub-millisecond, with a 30-second TTL on cached entries (PostgreSQL is queried only on a miss). Revoking, deleting, or updating a key evicts the cache entry immediately, so stale keys are never accepted.
  2. The resolved workspace identity is attached to the request for everything downstream: rate limiting, policy evaluation, budget checks, audit events, cost attribution, and structured logs.

You do not need to run a separate cache service. The distributed cache is built in and auto-clusters across nodes. See Caching.

Opaque tokens, not JWTs

Revocation is a single database row flip, immediate across the fleet. A JWT stays valid until expiry unless every verifier consults a blacklist — which defeats the point of a self-contained token.

Require API keys in multi-tenant production

By default, the data plane accepts requests with no bearer token and treats them as anonymous — no workspace is attached. In a multi-tenant deployment, that creates a silent escape hatch from workspace-scoped policies, budgets, rate limits, cost attribution, and audit. Set DVARA_LLM_GATEWAY_REQUIRE_API_KEY=true so keyless requests are rejected with HTTP 401.

3-level configuration hierarchy

Many features cascade across three levels — global, workspace, and API key. Every applicable level is evaluated; the strictest or most specific level wins.

Global ← applies to every workspace
└─ Workspace ← applies to every key in one workspace
└─ API Key ← applies to one key in one workspace
  • Budget caps stack at all three levels. A global "$5,000/month" plus a workspace "$500/month" plus an API key "$50/day" all apply, and the request is allowed only if it fits under every applicable cap.
  • Policies have global and workspace scopes. Both run on every request; the strictest decision wins.
  • Rate limits are keyed by API key, which transitively scopes them to the workspace.

Not every feature has all three levels. Where workspace-level config isn't supported, the global default applies to every workspace.

Per-workspace configuration

Per-workspace configuration is a small key-value map on each workspace. It overrides global defaults without a config reload.

Two ways to set it:

  • DVARA Flightdeck — go to Workspaces → Edit. Form tabs cover the Basic, PII, Guardrails, and Embedded scanner keys. See Workspaces & Users for the walkthrough.
  • GitOps — the workspace's metadata object in the Console's config export/import. Intended for version-controlled and IaC-driven configuration. Covers every key in the table below. The API merges into existing metadata — keys not in the request body are preserved.

Changes take effect on the next request. Keys are flat dot-separated strings; values are strings, booleans, numbers, or JSON-encoded maps.

Key (or prefix)TypeFeature
priority-tierpremium / standard / bulkPriority admission control
pat.max-ttl-daysint (≤ 365)Narrow the platform-wide personal access token TTL ceiling for this workspace
pii.enabled, pii.action, pii.scan-responses, pii.scan-streaming-responses, pii.custom-patternsvariousPII Detection
embedded.enabled-filtersCSV of filter namesPer-workspace built-in PII filter restriction
guardrail.enabled, guardrail.action, guardrail.risk-score-thresholdvariousGuardrails
guardrail.max-input-tokens, guardrail.max-messages-per-request, guardrail.max-message-length, guardrail.default-max-response-tokensintOWASP LLM10 input-size overrides
guardrail.scan-streaming-responsesbooleanStreaming content filter toggle
guardrail.content.* (profanity / violence / sexual / competitor / topic / custom denylist)variousPer-category content filtering
guardrail.injection.custom-patterns, guardrail.mcp-injection.enabled, guardrail.mcp-injection.actionvariousPrompt injection and MCP tool-injection detection
guardrail.context.warning-threshold-pct, guardrail.context.hard-threshold-pct, guardrail.context.pruning-strategyint / enumContext window management
guardrail.pluginsJSON mapPer-workspace guardrail plugin overrides
grounding.enabled, grounding.action, grounding.max-sources, grounding.max-source-lengthvariousHallucination Detection
cost.downgrade-threshold-pct, cost.downgrade-rules, cost.anomaly-threshold-pctvariousModel downgrade and FinOps anomaly sensitivity
ip-access.allowlist, ip-access.denylistCSV of CIDRsPer-workspace IP access control (CIDR ranges)
approval.required-tools, approval.required-servers, approval.timeout-seconds, approval.default-actionvariousMCP approval gates
agentic.loop-detection.*variousPer-workspace loop detection tuning
audit.store-promptsbooleanOpt-in prompt storage for compliance
Set these from the Console's workspace form, or as a `metadata` object on the workspace in the
Console's **GitOps** export/import document.

The per-workspace value accepts a boolean, a number (`0` / `1`), or a YAML 1.1 truthy / falsy string (`yes` / `no`, `on` / `off`, `1` / `0`, case-insensitive). Unrecognized values fall back to the global property and log a startup warning so a typo doesn't quietly disable enforcement.

Two operational notes worth flagging:

- **Pair with `DVARA_LLM_GATEWAY_REQUIRE_API_KEY=true`.** Strict-BYOK only fires when the request arrives with a workspace identity. Keyless anonymous traffic carries no workspace attribute and bypasses enforcement; the gateway logs a startup warning when the two switches are out of sync.
- **Rejection happens late.** A workspace under enforcement who lacks their own credential still consumes their per-key rate-limit window and budget for the failed call, since the gate runs inside the upstream HTTP path after policy and budget filters. Size rate-limit and budget defaults with a little headroom for onboarding rejections.

### Storage modes

Each credential is stored one of two ways:

- **Encrypted** (default) — the key is encrypted at rest with AES-256-GCM. The encryption key is derived from `DVARA_ENCRYPTION_MASTER_PASSWORD` using PBKDF2-HMAC-SHA256, so the master password is required whenever any credential uses this mode.
- **Reference** — only a vault pointer is stored in the database; the live secret never lands on the gateway at rest. Lookups go to HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault, and are cached in-process for 5 minutes by default (`dvara.vault.cache-ttl-seconds`), so steady-state traffic does not hit the vault per request. A deployment using only reference credentials can omit the master password entirely.

Full walkthrough: [Credentials & BYOK](../flight-deck/console/12-credentials-and-byok.mdx).

## RBAC: platform vs workspace scope

When RBAC is enabled (on by default whenever authentication is enabled), DVARA splits roles into three platform roles and three workspace roles:

| Role | Scope | Example user |
|---|---|---|
| `owner` | Platform — full access everywhere | Platform lead |
| `policy-admin` | Platform — policies, routes, guardrails, MCP servers | Security engineer |
| `billing-admin` | Platform — pricing, costs, budgets, chargeback, compliance reports | FinOps |
| `admin` | Workspace — full access within own workspace + user management | Team lead |
| `developer` | Workspace — create API keys, view usage, dry-run policies | Application developer |
| `viewer` | Workspace — read-only access within own workspace | Auditor |

**Mixing platform and workspace roles on one user is disallowed.** A user is either platform staff (can see any workspace's data) or a workspace user (can only see their own workspace's). This keeps audit attribution clean — every admin action traces unambiguously to one side or the other, without the confused middle ground of a user who is sometimes acting across workspaces and sometimes inside one. One shared validator enforces the rule at every sign-in surface: user creation + role-update (Admin REST + Console form), every OIDC JWT (the converter rejects mixed-role claim sets with HTTP 401 `invalid_token`), and every SAML assertion (the handler redirects to `/login?error=saml_role_mix_invalid` and refuses to seat the session). A misconfigured IdP cannot ship a mixed-role claim set into the security context.

Platform users land on DVARA Flightdeck at `/`; workspace users are auto-redirected to the DVARA Portal at `/portal` after login.

## Workspace scoping

Most Console list and summary pages carry an optional workspace filter — Budgets, Policies, Costs, Audit, Token Usage and the rest.

The scope rule is enforced consistently across every page, summary and write that accepts a workspace id from the client:

- A platform `owner`, `policy-admin`, or `billing-admin` may pass any workspace id, or omit it for a cross-workspace view.
- A workspace `admin`, `developer`, or `viewer` may only act on their own workspace. Omitting `workspace_id` narrows the result to their own workspace. Passing a different workspace's id is **rejected with HTTP 403** — the request is refused, not quietly narrowed, so the caller notices the misuse. A `WORKSPACE_SCOPE_VIOLATION` audit event is written on every rejection so security teams can detect probing.

The same rule applies to write paths that carry `tenantId` in a request body or path variable. Enforcement happens at parameter binding — the controller never sees a workspace id the caller is not authorized to act on, regardless of whether the value arrived as a query parameter, a JSON field, or a URL segment.

A workspace-scoped user who asks for another workspace's data — by editing the `workspace_id` in the URL, say — gets **`403`**, and the attempt is recorded as a `WORKSPACE_SCOPE_VIOLATION` audit event. The same user with no filter is narrowed to their own workspace and sees their own rows.

The narrowing is not a UI convenience: it happens at parameter binding, so it applies whether the request came from a rendered page or a hand-edited URL.

## Managing workspaces

### Creating a workspace

Create a workspace from the Console's **Workspaces** page (`/workspaces` → **New workspace**).
For bulk provisioning, add it to the GitOps export document and import that — the import
reconciles by id, so re-importing the same document is safe.

Or interactively from DVARA Flightdeck under **Workspaces → New Workspace** — see [Workspaces & Users](../flight-deck/console/04-workspaces-and-users.mdx) for the UI walkthrough. After creation, issue at least one API key for the workspace so applications can start sending traffic.

### Suspending vs deleting

- **Suspend** (reversible)set `status: SUSPENDED`. New requests are rejected with a clear error code, but audit events, cost records, and token usage stay intact. You can re-activate later without losing history. Right for free-trial expirations, billing disputes, and short-term offboarding.
- **Delete** (irreversible) — see the surface table below for who can call which endpoint. The endpoint **cascades**: the per-workspace rows in 13 data tables (API keys, policies, budgets, webhooks, MCP servers, prompt templates / experiments, token usage, cost records, shadow policy events, chargeback / compliance reports, provider credentials, guardrail plugins) are cleared first, then the identity tables (users and API keys) are cleared, then the workspace row. The `WORKSPACE_DELETED` audit event payload includes `data_rows_deleted` / `users_deleted` / `keys_deleted` counts so operators can confirm the cascade scope. **Append-only audit records** — audit events and MCP tool-call logs — are **retained by design**: they reference the workspace by id rather than a foreign key, so they survive the delete as historical records you can still investigate or export from. Platform-global rows — shared provider credentials, guardrail plugins, policies, and budget caps that belong to no single workspace — are never touched.

Deleting a workspace is an operator action, handled from the DVARA Console:

| Surface | Role required | Behaviour |
|---|---|---|
| **Delete** on the workspace's row in the Console | Platform `owner` only | Immediate. Cascades through the data + identity tables described above. No recovery. |

Other platform roles (`policy-admin`, `billing-admin`) cannot delete a workspace.

The SaaS self-delete path runs the same workspace data purge under the hood, plus an additional PII-token purge for compliance-grade right-to-be-forgotten.

For permanent offboarding with a compliance requirement to purge PII, delete the workspace and then purge its PII tokens explicitly from the Console's **PII** page — deletion removes the workspace's rows, and the token purge makes any tokens minted under it permanently unresolvable.

## Related

- **[DVARA Flightdeck — Workspaces & Users](../flight-deck/console/04-workspaces-and-users.mdx)** — UI walkthrough for workspace CRUD and API key management
- **[DVARA Flightdeck — Credentials & BYOK](../flight-deck/console/12-credentials-and-byok.mdx)** — full credential management flow, including reference storage mode
- **[Configuration](../deployment/99-configuration.md)** — property reference for every feature that has per-workspace overrides