Skip to main content

Multi-tenancy

Riverbase isolates tenants at the row level. Every framework-managed row has a _tenant UUID. Two configurable policies decide what goes into it and who can see it.

Two policies​

PolicyDecidesConfigDefault
Stamp policyWhich UUID is written to _tenant when a row is createdtenant_stamp_policy / RIVERBASE_TENANT_STAMP_POLICYprofile-organization
Access policyWhich _tenant values a caller may read or changetenant_access_policy / RIVERBASE_TENANT_ACCESS_POLICYprofile-organization

Built-in names:

NameStamp writesAccess allows
profile-organizationThe active profile's organizationThat organization
profileThe active profile idThat profile
userThe login user idThat user
domain-tenantThe tenant mapped to the domain's namespaceThat tenant
system(no stamp; not valid)All tenants. The _tenant predicate is omitted. For operator and coordinator portals only.

Applications can register more policies with RiverbaseApp::register_tenant_access_policy, and map namespaces to tenants with set_domain_tenant_lookup.

What the framework does for you​

  • Create: the stamp policy writes _tenant and overwrites anything the payload sent.
  • Update: the stored _tenant is carried over verbatim. Updates never re-stamp, and a client cannot move a row by sending _tenant.
  • Load in an aggregate: a row from a tenant outside the caller's access list is invisible. To the caller it does not exist.
  • Query: _tenant IN (…access list…) is ANDed into the server policy filter.
  • Audit: context, command, event, activity, message, and query records persist _tenant.

An empty access list matches no rows. When a tenant cannot be resolved, the result is nothing, not everything.

Opting a resource out​

A catalog shared by every tenant, such as reference data, opts out of the row filter. It still needs a reason and a sign-off reference:

riverbase_core::query_resource! {
CountryQueryResource name "country" {
// … meta, fields, binding, errors …
policy: public("reference data shared by every tenant", "SEC-12"),
tenant_scope: exempt("reference data shared by every tenant", "SEC-12"),
}
}

policy decides whether the resource needs a scope_policy constraint. tenant_scope: exempt removes the automatic _tenant filter. They are separate decisions, and a shared catalog usually needs both.

Moving a row between tenants​

Transfers are explicit operations, never a side effect of an update. The framework compares the destination with the stored tenant (TenantTransfer::Unchanged when they match) so a transfer command can be idempotent.

Choosing a setup​

Product shapeStampAccess
B2B SaaS, users act for an organizationprofile-organizationprofile-organization
Personal data, each user owns their rowsuseruser
One tenant per deployment or per domaindomain-tenantdomain-tenant
Back-office portal over all tenants(as the product)system

Related: Permissions tutorial · Configuration reference.