Multi-Tenancy

Rivano is a multi-tenant platform. Every resource — agents, policies, traces, teams, keys — belongs to exactly one tenant. Tenant boundaries are enforced at the database layer using Postgres row-level security, not application-level filtering.

Tenant model

A tenant is created when the first user signs up. All subsequent team members join that tenant. Each tenant has:

  • A unique UUID (tenant_id)
  • Its own agent registry, policy set, and trace history
  • Its own team with independent role assignments
  • Isolated billing and usage metering

There is no cross-tenant resource sharing. You cannot grant access to a resource in tenant A to a user in tenant B.

Row-level security

All tables that contain tenant data include a tenant_id column. The Postgres RLS policy on each table enforces:

-- Example: every query automatically filters to the current tenant
CREATE POLICY tenant_isolation ON traces
  USING (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

The application sets app.tenant_id at the start of every database transaction using set_config('app.tenant_id', tenantId, true). If the setting is absent or the tenant ID does not match, the row is invisible to the query.

This means a bug in application-layer authorization cannot expose cross-tenant data — the database enforces the boundary independently.

Context resolution

Every inbound request — dashboard session, API key call, or gateway proxy request — is resolved to a tenant and user context before any handler runs.

Dashboard session cookie — The session record contains the user ID. Rivano loads the user row, which includes tenant_id and role. This context is set for the transaction.

API key — The key is hashed and looked up in the api_keys table, which contains tenant_id, scope, and created_by. The tenant context is set from the key record.

Gateway proxy request — The gateway authenticates with an ingest-scoped key. The tenant context flows from the key, and the trace is written to that tenant’s partition.

💡

API keys carry the tenant context of the user who created them. A key created by a member in tenant A always writes to tenant A, regardless of which service is using the key.

Data boundaries

The following data never crosses tenant boundaries:

ResourceIsolation mechanism
AgentsRLS on agents table
PoliciesRLS on policies table
Traces & spansRLS on traces, trace_spans tables
Proxy cacheRLS on proxy_cache table
Compliance reportsRLS on compliance_reports table
API keysRLS on api_keys table
Audit / governance changelogRLS on governance_changelog table

Billing data is additionally isolated at the Stripe customer level — each tenant maps to a distinct Stripe customer ID.

Team scoping

Within a tenant, access can be further scoped to sub-teams. A team is a named group of users with a set of permitted scopes. Teams are useful for large organizations where different groups manage different agents.

For example, you might create a platform team that has access to all policies and a product team that has read-only access to traces for specific agents.

Team scopes are applied on top of the user’s tenant-level role. A user can be a member at the tenant level but have elevated access to specific resources via team membership.

See RBAC for the full permission model and team scope configuration.

OIDC session isolation

OIDC sessions (oidc_sessions table) are scoped by tenant_id and user_id. The OIDC state parameter includes the tenant context so that callback validation cannot be replayed across tenants.