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:
| Resource | Isolation mechanism |
|---|---|
| Agents | RLS on agents table |
| Policies | RLS on policies table |
| Traces & spans | RLS on traces, trace_spans tables |
| Proxy cache | RLS on proxy_cache table |
| Compliance reports | RLS on compliance_reports table |
| API keys | RLS on api_keys table |
| Audit / governance changelog | RLS 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.
Related
- RBAC — Permission model and team scoping
- Authentication — How tenant context flows from auth tokens
- Security Overview — Defense-in-depth architecture