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.
Tenant identity
Section titled “Tenant identity”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.
Tenant isolation model
Section titled “Tenant isolation model”tenant_id scoping on all resources
Section titled “tenant_id scoping on all resources”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:
| Resource | Table | Isolation field |
|---|---|---|
| Users | users |
tenant_id |
| Groups | groups |
tenant_id |
| API keys | api_keys |
tenant_id |
| Audit log entries | audit_logs |
tenant_id |
| IP allowlist entries | ip_allowlist_entries |
tenant_id |
| Policy chains | policy_pack_chains |
scope_id (org UUID) |
| Policy packs | policy_packs |
tenant_id |
| Policy rules | policy_rules |
(scoped via pack → chain) |
| DLP rules | org_dlp_rules |
org_id |
| Compliance bundle bindings | org_compliance_bundles |
org_id |
| SCIM tokens | org_scim_tokens |
org_id |
| Outpost audit sync entries | audit_logs |
tenant_id + outpost_id |
Authentication and tenant context
Section titled “Authentication and tenant context”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.
Audit log isolation
Section titled “Audit log isolation”The audit log search API (GET /api/v1/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.
JWT-scoped authentication
Section titled “JWT-scoped authentication”JWT claims and tenant context extraction
Section titled “JWT claims and tenant context extraction”Arbitex JWTs are HS256 (user tokens) or RS256 (M2M tokens). Neither format embeds tenant_id directly in the token payload. Instead, tenant context is loaded from the database on every authenticated request.
The access token payload contains:
| Claim | Value | Purpose |
|---|---|---|
sub |
User UUID string | Identifies the authenticated user |
role |
"admin" or "user" |
Role claim for fast authorization checks |
jti |
UUID string | JWT ID for per-token revocation |
type |
"access" |
Distinguishes access tokens from refresh/MFA tokens |
iat |
Unix timestamp | Issued-at — used for bulk revocation check |
exp |
Unix timestamp | Expiry |
mfa_verified |
Boolean (optional) | Present and true if MFA was completed in this session |
passkey_enrollment_required |
Boolean (optional) | Present and true if org policy requires passkey and user has none |
The tenant_id is not in the token. It is loaded from users.tenant_id after token validation. This is a deliberate security design: a compromised token cannot claim a different tenant — the tenant binding lives in the database, not the token.
Extraction flow in get_current_user (backend/app/core/dependencies.py):
# 1. Decode and verify JWT (HS256 or RS256 depending on alg header)payload = decode_access_token(token)
# 2. Check JTI revocation via bloom-filter-accelerated blacklistjti = payload.get("jti")if jti and await blacklist_backend.is_blacklisted(jti, db): raise HTTPException(401, "Token has been revoked")
# 3. Load user by sub claim — tenant_id is on the user rowuser_id = uuid.UUID(payload.get("sub"))user = await db.execute(select(User).where(User.id == user_id))
# user.tenant_id is now the authoritative tenant context for the requestAPI key authentication and tenant lookup
Section titled “API key authentication and tenant lookup”API keys use the X-API-Key header or Authorization: ApiKey <key> prefix. Key authentication resolves to a User record with the same tenant_id semantics as JWT authentication.
Key format: mmx_<32 hex chars> (40 characters total). Keys are stored as SHA-256 hashes (api_key_auth.py):
# Lookup: hash incoming key, find matching APIKey rowkey_hash = hashlib.sha256(key.encode("utf-8")).hexdigest()api_key_record = await db.execute( select(APIKey).where(APIKey.key_hash == key_hash))# Resolve to User — tenant_id comes from user rowuser = await db.execute(select(User).where(User.id == api_key_record.user_id))# user.tenant_id anchors the request to the correct orgAuthentication precedence in get_current_user:
X-API-Keyheader checked firstAuthorization: ApiKey <key>prefix checked second- JWT Bearer token checked last
Regardless of auth method, the resolved User.tenant_id is the authoritative tenant context. There is no way to authenticate as a user in one org and operate against resources in another.
MFA claims and per-org MFA enforcement
Section titled “MFA claims and per-org MFA enforcement”MFA enforcement is configured per-org (org_mfa_policies table, enforcement_level field):
"off"— MFA is not enforced (default)"optional"— MFA is encouraged but not required"required"— All sensitive endpoint requests must carry a JWT withmfa_verified=True; requests without this claim are rejected with HTTP 403
When MFA is required and a user authenticates with password only, the login flow returns a short-lived MFA challenge token (type: "mfa_challenge", 5-minute expiry). The user must complete TOTP verification before a full access token with mfa_verified: true is issued. This claim travels with the JWT for the duration of the session.
Passkey enforcement is similarly per-org (org_passkey_policies table, passkey_level field: "off" | "encouraged" | "required"). When "required" and the user has no registered passkey, the access token carries passkey_enrollment_required: true, triggering the mandatory enrollment flow in the frontend.
API key isolation
Section titled “API key isolation”Per-org key pools
Section titled “Per-org key pools”API keys are scoped to a tenant_id via the owning user. All keys belonging to users in org A are isolated from org B — there is no cross-org key sharing. Keys carry no org-level grant; they authenticate as a specific user whose tenant_id determines the tenant scope.
On the Cloud portal side, the api_keys table (cloud/db/models.py) uses a direct org_id FK to organizations.id:
class ApiKey(Base): __tablename__ = "api_keys" org_id = Column(String(36), ForeignKey("organizations.id", ondelete="CASCADE"), ...) key_hash = Column(String(64), ...) # SHA-256 hex digest scopes = Column(JSON, ...) # JSON array of scope stringsThis means cloud-plane API keys are org-bound at the row level. Deleting or suspending an org cascades to its API key rows.
Cross-org key rejection
Section titled “Cross-org key rejection”The authentication path makes cross-org key use structurally impossible. Presenting an API key from org A against an endpoint scoped to org B produces HTTP 401 — the key’s SHA-256 hash either resolves to a user in org A (query succeeds, but the user’s tenant_id is org A, so all queries are filtered to org A) or to no user at all. There is no pathway by which a key from org A returns data from org B.
Key rotation isolation
Section titled “Key rotation isolation”API keys are individually revocable. The is_enabled flag and expires_at column on each key row allow rotation without affecting other keys or other orgs. On the platform side, rotating a key in org A involves no state change in org B’s tables — there is no shared key pool.
SCIM tokens have a distinct rotation model (org_scim_tokens table): the active token is identified by rotated_at IS NULL. On rotation, the previous token row gets rotated_at set to the rotation timestamp (preserving history for audit), and a new row is inserted. The raw token is returned exactly once at rotation time and never stored. Rotation in one org does not touch rows for any other org.
Per-org configuration
Section titled “Per-org configuration”Each org has an independent configuration surface. No configuration is shared between orgs.
Policy chains and packs
Section titled “Policy chains and packs”Policy evaluation uses two chain scopes:
scope="org"— the org-level policy chain, applied to all users in the orgscope="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.
DLP rules
Section titled “DLP rules”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.
Provider access and model routing
Section titled “Provider access and model routing”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.
Budget and quota controls
Section titled “Budget and quota controls”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.
SCIM provisioning
Section titled “SCIM provisioning”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.
IP allowlist
Section titled “IP allowlist”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.
DLP configuration isolation
Section titled “DLP configuration isolation”Per-org detector configuration
Section titled “Per-org detector configuration”DLP configuration is isolated at two levels: the org can add custom detectors and can suppress specific platform-level detectors. Neither change affects any other org.
The OrgDLPLayer class (backend/app/core/org_dlp_layer.py) is the composable integration point. It is instantiated per-request with the org’s UUID:
layer = OrgDLPLayer(org_id)await layer.load(db) # loads rules via 60-second TTL cacheOn load, rules from org_dlp_rules are separated into two groups:
suppress_defaultrules — store the platform pattern name to exclude from resultscustom_patternrules — regex patterns compiled withsafe_compile()(timeout-protected)
Rule precedence: platform then org
Section titled “Rule precedence: platform then org”The DLP pipeline runs in this order:
- Platform scan — global
dlp_rules(noorg_id) are evaluated first. Results are a list ofDLPMatchobjects. - Org suppression filter —
layer.filter_platform_matches(matches)removes any match whosedetector_nameis in the org’s suppressed set. - Org custom patterns —
await layer.scan_custom_patterns(text)evaluates the org’s compiled regex patterns and produces additionalDLPMatchobjects. - Merge — filtered platform matches and org custom matches are combined for downstream action evaluation.
Custom pattern match names use the format org_custom:{rule_name}:{action_tier} in the detector_name field, making org-sourced matches distinguishable from platform matches in audit logs.
Compliance bundle interaction
Section titled “Compliance bundle interaction”When a compliance bundle is active for an org (org_compliance_bundles table), its bundle rules are evaluated during DLP scanning and cannot be suppressed by org custom rules. The disabled_at column on OrgComplianceBundle supports soft-disable while preserving activation history for audit purposes. Bundle definitions are global (platform-level); per-org bindings in org_compliance_bundles activate them for individual orgs.
Usage metering isolation
Section titled “Usage metering isolation”Per-org usage counters
Section titled “Per-org usage counters”Usage is tracked in org_usage_counters (one row per org, OrgUsageCounter model). The counter row stores:
| Field | Description |
|---|---|
org_id |
Unique — one row per org (enforced by uq_org_usage_counters_org_id) |
plan_tier |
Denormalized tier key for fast limit lookup without a join |
request_count |
Cumulative requests in the current billing period |
period_start / period_end |
First and last day of the current billing month |
last_request_at |
Timestamp of the most recent request increment |
Plan tiers (PlanTier enum) and their request limits:
| Tier | Key | Monthly request limit |
|---|---|---|
| Developer Free | devfree_saas |
100,000 |
| Developer Pro | devpro_saas |
1,000,000 |
| Team | team_saas |
1,000,000 |
| Enterprise SaaS | enterprise_saas |
Custom |
| Enterprise Outpost | enterprise_outpost |
Custom |
The service performs atomic upsert increments (INSERT ... ON CONFLICT) on the counter row. When period_start falls before the first day of the current calendar month, the service resets request_count to 0 and updates the period columns — this is the billing rollover. Usage from one org does not affect another org’s counter.
Usage rollups
Section titled “Usage rollups”The org_usage_rollups table materializes aggregated usage at three granularities per org:
period_type |
Window |
|---|---|
hourly |
1-hour window |
daily |
24-hour window |
monthly |
Calendar month |
Each rollup row is keyed by (org_id, period_type, period_start) — a unique constraint ensures idempotent upserts. The model_breakdown JSONB column stores per-model statistics (request count, input tokens, output tokens, cost) for the window. This enables org-specific usage dashboards and per-model cost attribution without cross-tenant data exposure.
Budget enforcement in the Outpost
Section titled “Budget enforcement in the Outpost”The Outpost reads budget caps from the signed policy bundle delivered by the Platform. Budget enforcement is local to the Outpost instance, which is org-bound. The budget_enforcement_enabled configuration flag controls whether budget checks are active (default: True). When a budget cap is hit, the Outpost blocks requests for that org without impacting other org instances.
Data isolation guarantees
Section titled “Data isolation guarantees”What is isolated per-org
Section titled “What is isolated per-org”| Data type | Isolation mechanism |
|---|---|
| Conversations and messages | tenant_id on conversations and messages tables |
| Audit trail | tenant_id filter on all queries; HMAC chain is per-org sequence |
| API keys | tenant_id on api_keys; keys can only authenticate users in the same org |
| Groups and memberships | tenant_id on groups; group policies apply only within the org |
| Users | tenant_id on users; cross-org user queries are not possible |
| Policy engine configuration | Chain scope uses org UUID as scope_id |
| DLP configuration | org_id on custom rules; org_id on compliance bundle bindings |
| SIEM configuration | tenant_id on SIEM connector configs |
| Compliance bundle state | Per-org enable/disable binding in org_compliance_bundles |
| MFA policy | org_id on org_mfa_policies — one row per org |
| Passkey policy | org_id on org_passkey_policies — one row per org |
| Usage counters | org_id on org_usage_counters — one row per org |
| Usage rollups | org_id on org_usage_rollups — rows keyed by org |
| IP allowlist | org_id on org_ip_allowlists — entries are per-org |
What is shared across orgs (platform-level)
Section titled “What is shared across orgs (platform-level)”| Shared resource | Notes |
|---|---|
| Platform-level DLP patterns | dlp_rules without org_id — available to all orgs, read-only |
| Compliance bundle definitions | Bundle templates are global; per-org bindings activate them |
| Model catalog | Provider configurations and model availability are platform-level |
| GeoIP / CredInt databases | Shared MaxMind and CredInt Bloom filter databases |
| Infrastructure | Kubernetes cluster, PostgreSQL instance (separate databases per environment, row-level tenant isolation within tables) |
What is NOT stored per-org
Section titled “What is NOT stored per-org”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.
Cross-tenant protections
Section titled “Cross-tenant protections”Middleware enforcement
Section titled “Middleware enforcement”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.
Query filtering
Section titled “Query filtering”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.
Admin dependency
Section titled “Admin dependency”The require_admin FastAPI dependency enforces both role and tenant scope. It:
- Verifies the JWT is valid and the user exists
- Checks
user.role == "admin" - Loads the user’s
tenant_idfor 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.
SCIM token isolation
Section titled “SCIM token isolation”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.
Admin plane separation
Section titled “Admin plane separation”Org admin vs Arbitex staff roles
Section titled “Org admin vs Arbitex staff roles”The platform user model has two roles: "admin" and "user" (defined in UserRole enum, backend/app/models/user.py). The role=admin designation is org-scoped — it grants admin capabilities within the user’s own tenant only. There is no application-layer global admin role.
| Capability | role=user |
role=admin (org-scoped) |
|---|---|---|
| Read own conversations | Yes | Yes |
| Manage own API keys | Yes | Yes |
| View org audit logs | No | Yes (own org only) |
| Manage org users and groups | No | Yes (own org only) |
| Configure policy chains | No | Yes (own org only) |
| Activate compliance bundles | No | Yes (own org only) |
| Configure SCIM provisioning | No | Yes (own org only) |
| Configure IP allowlist | No | Yes (own org only) |
| Access another org’s data | No | No |
Org admin access to any admin endpoint is verified by require_admin (core/dependencies.py), which both checks role == "admin" and anchors the session to the user’s tenant_id. An org admin cannot escalate to access another org.
Cloud portal admin roles
Section titled “Cloud portal admin roles”The Cloud portal (arbitex-cloud) has a separate admin role model in org_admins table (cloud/db/models.py). Portal admin roles are:
| Role | Key | Scope |
|---|---|---|
| Owner | owner |
Full control of the org in the portal; can invite/remove admins |
| Admin | admin |
Org admin in the portal |
| Member | member |
Read-only access to portal org settings |
These roles govern access to the Cloud portal (outpost management, billing, org provisioning). They are distinct from the platform UserRole admin designation that governs access to platform API admin endpoints.
Arbitex staff operations
Section titled “Arbitex staff operations”The customer admin API endpoints enforce tenant_id filtering — Arbitex staff cannot use the customer API to access data across organizations. There is no “super-admin” bypass in the customer API surface.
Token revocation at the org level
Section titled “Token revocation at the org level”The Cloud portal (cloud/db/models.py) supports bulk revocation via the tokens_revoked_before column on the Organization table. Setting this to a timestamp invalidates all tokens issued before that time for the org. Individual token revocation uses the jwt_revocations table keyed by jti. Both checks are performed on every authenticated request in get_org_context (cloud/api/deps.py):
# 1. Individual jti revocationrevoked = await db.execute( select(JwtRevocation).where(JwtRevocation.jti == jti))# 2. Bulk revocation: token iat must be > org.tokens_revoked_beforeorg_result = await db.execute( select(Organization.tokens_revoked_before).where(Organization.id == sub))A Redis cache (keyed by jti) accelerates repeat-request validation within the token’s remaining TTL. The cache is invalidated on revocation events.
Outpost tenant binding
Section titled “Outpost tenant binding”org_id in configuration and heartbeat
Section titled “org_id in configuration and heartbeat”Each Outpost instance is bound to exactly one org. The org_id is an explicit configuration field (OutpostSettings, outpost/config.py):
org_id: str = Field( default="", description="Organisation UUID. Required for Cloud API paths (heartbeat, etc.).",)This org_id is required for all management plane operations. If it is not set, heartbeat and policy sync are skipped:
if not self.settings.platform_management_url or not self.settings.outpost_id or not self.settings.org_id: logger.debug("Heartbeat skipped — platform URL, outpost ID, or org ID not configured") return FalseThe heartbeat endpoint URL is built from org_id:
POST /v1/orgs/{org_id}/outposts/{outpost_id}/heartbeatThis URL structure means the Platform can verify at the routing layer that the outpost_id in the path belongs to the org_id in the path, cross-checking against the tenant_id embedded in the presenting mTLS certificate.
Certificate-bound tenant identity
Section titled “Certificate-bound tenant identity”When an Outpost registers, the Cloud control plane issues a client certificate with tenant_id embedded. Subsequent communications use mTLS — the Outpost presents this certificate on every connection. The ssl.SSLContext is built with check_hostname = True and verify_mode = ssl.CERT_REQUIRED, preventing unverified connections:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)ctx.load_verify_locations(cafile=ca_path)ctx.check_hostname = Truectx.verify_mode = ssl.CERT_REQUIREDThe outpost will not start without the three certificate files (OUTPOST_CERT_PATH, OUTPOST_KEY_PATH, OUTPOST_CA_PATH). There is no fallback to unverified connections.
Signed policy bundles
Section titled “Signed policy bundles”The policy bundle delivered to each Outpost at GET /v1/internal/org/{org_id}/policy is bound to the requesting org by the URL structure. The bundle is additionally protected by HMAC-SHA256 (POLICY_HMAC_KEY) for tamper detection:
The canonical form for HMAC verification is:
canonical_bundle = {k: v for k, v in bundle.items() if k != "bundle_hmac"}canonical_json = json.dumps(canonical_bundle, sort_keys=True, separators=(",", ":"))expected_hmac = hmac.new(key.encode(), canonical_json.encode(), hashlib.sha256).hexdigest()Verification additionally enforces a freshness check: bundles older than 3600 seconds (1 hour) based on generated_at or synced_at are rejected, even if the HMAC is valid. A tampered or stale bundle is never applied — the Outpost continues operating with its cached bundle rather than applying an unverified one.
Provider API keys within the bundle can be encrypted with a Fernet key (PROVIDER_KEY_ENCRYPTION_KEY). When set, api_key_encrypted fields are decrypted at runtime; plaintext api_key fields are used only in dev mode.
Policy sync and config sync
Section titled “Policy sync and config sync”Policy sync uses the same mTLS client as heartbeat. The policy delivered is exactly the chain for the Outpost’s org — no other org’s policies are ever included in a bundle. The Outpost’s org_id is embedded in the sync URL:
GET /v1/internal/org/{org_id}/policyETag-based conditional requests (If-None-Match) mean the Outpost only processes a new bundle when the policy has actually changed. The cached bundle is written atomically (write-to-temp, then rename) with 0o600 permissions so only the outpost process owner can read it.
Org lifecycle
Section titled “Org lifecycle”Creation
Section titled “Creation”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)
Configuration
Section titled “Configuration”After creation, org admins:
- Configure authentication (SAML IdP, OIDC, or local)
- Set up SCIM provisioning (optional)
- Create groups and assign users
- Activate compliance bundles and build policy chains
- Configure model access and routing rules
- Optionally configure IP allowlisting
Configuration changes (policy updates, user management, compliance bundle toggles) are audit-logged with the admin user ID and timestamp.
Suspension
Section titled “Suspension”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.
Data deletion
Section titled “Data deletion”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).
Hybrid Outpost tenant model
Section titled “Hybrid Outpost tenant model”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.
Registration
Section titled “Registration”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.
Policy sync
Section titled “Policy sync”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.
Audit sync
Section titled “Audit sync”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.
Data residency
Section titled “Data residency”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).
See also
Section titled “See also”- Request lifecycle architecture — End-to-end request flow through tenant boundaries
- SCIM provisioning guide — Automated user provisioning per org
- IP Allowlist Administration — Per-org IP restriction configuration
- Audit Data Model Reference — Audit log schema and tenant_id scoping
- Security Overview — Cross-cutting security controls
- Outpost architecture — Hybrid Outpost deployment model