Skip to content

Multi-Tenancy Architecture

Arbitex is a multi-tenant system. Each customer organization (org) is a fully isolated tenant. This reference describes the isolation model, per-org configuration surface, data isolation guarantees, and how the Hybrid Outpost extends tenancy to on-premises deployments.


An org (organization) is the top-level tenant unit in Arbitex. Every org has a UUID, referred to throughout the system as org_id or tenant_id. These identifiers are interchangeable — tenant_id is the field name used in database models; org_id is used in API paths.

There is no organizations table — org identity is carried by tenant_id on every scoped resource. All data access is filtered by tenant_id at the query layer. There is no application-level object that “owns” a tenant; isolation is enforced by the presence of tenant_id on every resource.


Every resource that is tenant-scoped carries a tenant_id (UUID) column. Resources are isolated at the database query layer — every query that accesses tenant data filters by tenant_id == current_user.tenant_id. Cross-tenant access is structurally prevented by this pattern.

Tenant-scoped resources:

ResourceTableIsolation field
Usersuserstenant_id
Groupsgroupstenant_id
API keysapi_keystenant_id
Audit log entriesaudit_logstenant_id
IP allowlist entriesip_allowlist_entriestenant_id
Policy chainspolicy_pack_chainsscope_id (org UUID)
Policy packspolicy_packstenant_id
Policy rulespolicy_rules(scoped via pack → chain)
DLP rulesorg_dlp_rulesorg_id
Compliance bundle bindingsorg_compliance_bundlesorg_id
SCIM tokensorg_scim_tokensorg_id
Outpost audit sync entriesaudit_logstenant_id + outpost_id

When a user authenticates (JWT or API key), the platform loads the tenant_id from the user record and places it in the request scope state. This value flows through all middleware and route handlers as the authoritative tenant identifier for the request.

The require_admin dependency verifies that the user’s role == "admin" and that their tenant_id matches the resource being accessed. Admin privileges are scoped to a single tenant — there is no global admin role in the application layer.

The audit log search API (GET /api/admin/audit-logs/) enforces tenant scoping explicitly:

base_query = select(AuditLog).where(AuditLog.tenant_id == current_user.tenant_id)

An admin cannot query audit entries for any other org. The tenant_id filter is applied before any user-supplied filter parameters — it cannot be overridden by the API caller.


Each org has an independent configuration surface. No configuration is shared between orgs.

Policy evaluation uses two chain scopes:

  • scope="org" — the org-level policy chain, applied to all users in the org
  • scope="user" — per-user override chain (personal policy adjustments)

The org chain is the primary enforcement mechanism. Rules use user_groups conditions to target specific groups within the org. Group membership is a condition on rules, not a separate chain scope.

Policy packs (bundles) are either:

  • Compliance bundles — pre-built packs locked for editing (is_bundle=true)
  • Custom packs — org-created packs with admin-editable rules

Policy chains and packs are scoped to tenant_id. A pack created by one org is invisible to all other orgs.

Org-level DLP rules (org_dlp_rules table, org_id column) layer on top of platform-level patterns. Each org can add custom detection patterns without affecting other tenants. Platform DLP patterns are global (no tenant_id) and run before org-specific rules.

Model access controls are configured per group within an org (group_model_access table). Routing rules (routing_rules table) are scoped to tenant_id. An org’s routing rules, fallback chains, and latency thresholds are independent of all other orgs.

Usage quotas (quota table) are scoped per-org. Token budgets, daily request limits, and cost caps apply within a single org’s usage counters (org_usage_counter table, org_id column). Usage from one org does not consume quota from another.

Per-org SCIM tokens (org_scim_tokens table, org_id column) are stored as bcrypt hashes. Each org has its own SCIM bearer token. Token rotation on one org does not affect any other org’s provisioning. See SCIM provisioning guide for configuration details.

Per-org IP allowlist entries (ip_allowlist_entries.tenant_id) are checked in isolation. An org with IP restrictions does not affect access for users in other orgs. An org with no allowlist entries permits all source IPs, regardless of what other orgs have configured.


Data typeIsolation mechanism
Conversations and messagestenant_id on conversations and messages tables
Audit trailtenant_id filter on all queries; HMAC chain is per-org sequence
API keystenant_id on api_keys; keys can only authenticate users in the same org
Groups and membershipstenant_id on groups; group policies apply only within the org
Userstenant_id on users; cross-org user queries are not possible
Policy engine configurationChain scope uses org UUID as scope_id
DLP configurationorg_id on custom rules; org_id on compliance bundle bindings
SIEM configurationtenant_id on SIEM connector configs
Compliance bundle statePer-org enable/disable binding in org_compliance_bundles

What is shared across orgs (platform-level)

Section titled “What is shared across orgs (platform-level)”
Shared resourceNotes
Platform-level DLP patternsdlp_rules without org_id — available to all orgs, read-only
Compliance bundle definitionsBundle templates are global; per-org bindings activate them
Model catalogProvider configurations and model availability are platform-level
GeoIP / CredInt databasesShared MaxMind and CredInt Bloom filter databases
InfrastructureKubernetes cluster, PostgreSQL instance (separate databases per environment, row-level tenant isolation within tables)

Arbitex does not store prompt or completion content by default. When prompt_text and response_text appear in audit entries, they are present only when prompt retention is explicitly enabled for the org. The default behavior is to omit this content from audit entries — only the metadata (token counts, model, provider, action, DLP category match) is retained.


IP allowlist middleware reads org context from scope["state"]["org_id"], which is set by authentication middleware from the authenticated user’s tenant_id. An attacker cannot specify a different org_id — it is derived from the authenticated JWT or API key, not from request parameters.

No query in the platform codebase uses unbounded access to tenant-scoped tables. All queries that access users, groups, audit logs, policy configurations, or API keys include an explicit tenant_id filter as part of the base query construction. The filter is applied before any user-supplied filters.

The require_admin FastAPI dependency enforces both role and tenant scope. It:

  1. Verifies the JWT is valid and the user exists
  2. Checks user.role == "admin"
  3. Loads the user’s tenant_id for downstream use in query scoping

A user with role=admin in org A cannot access resources in org B — the tenant_id carried in their JWT anchors all queries to their own org.

Per-org SCIM tokens (org_scim_tokens) use org_id as the scope key. Token verification looks up only WHERE org_id = ? for the specific org in the path parameter. Presenting a valid SCIM token for org A against org B’s SCIM endpoint returns 401.


Org creation assigns a UUID as the tenant_id. Initial configuration:

  • No users (first admin is invited post-creation)
  • No policy chain (platform default ALLOW applies)
  • No DLP configuration
  • No compliance bundles active
  • No IP allowlist (all IPs permitted)

After creation, org admins:

  1. Configure authentication (SAML IdP, OIDC, or local)
  2. Set up SCIM provisioning (optional)
  3. Create groups and assign users
  4. Activate compliance bundles and build policy chains
  5. Configure model access and routing rules
  6. Optionally configure IP allowlisting

Configuration changes (policy updates, user management, compliance bundle toggles) are audit-logged with the admin user ID and timestamp.

When an org is suspended, user.is_active = false is set for all users in the org. API keys and SCIM tokens continue to exist in the database but authentication fails because user accounts are inactive. Data is retained intact for the retention period.

On org deletion, all tenant-scoped rows are removed (cascades and SET NULL foreign keys preserve audit log integrity — user_id is set to NULL on audit entries rather than deleting the entries).


The Hybrid Outpost is a self-hosted Gateway deployment that connects to the Arbitex Cloud control plane. Each Outpost instance maps to a single org via the org’s tenant_id.

When an Outpost registers with the Cloud control plane (POST /cloud/api/outposts/register), it is issued a client certificate with tenant_id embedded. Subsequent communications from the Outpost present this certificate for mTLS authentication. The tenant_id in the certificate anchors all Outpost operations to the correct org.

The Cloud control plane syncs the org’s policy chain to the Outpost on registration and on each heartbeat (PUT /cloud/api/outposts/{outpost_id}/heartbeat). The policy chain delivered is exactly the chain for the Outpost’s org — no other org’s policies are ever sent to an Outpost instance.

Outpost-generated audit events are synced to the Cloud control plane and stored with:

  • tenant_id = the org’s UUID (same as cloud-generated entries)
  • source = "outpost"
  • outpost_id = the Outpost instance UUID

The HMAC chain covers Outpost-synced entries identically to cloud entries. The audit log presents a unified, chronologically ordered view of all events for the org regardless of origin.

In an air-gap deployment, Outpost audit events remain on-premises until sync occurs. For organizations with data residency requirements, the audit sync interval can be configured, or sync can be disabled entirely (the Outpost then provides local-only audit access via its own API).