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
| Policy | Decides | Config | Default |
|---|---|---|---|
| Stamp policy | Which UUID is written to _tenant when a row is created | tenant_stamp_policy / RIVERBASE_TENANT_STAMP_POLICY | profile-organization |
| Access policy | Which _tenant values a caller may read or change | tenant_access_policy / RIVERBASE_TENANT_ACCESS_POLICY | profile-organization |
Built-in names:
| Name | Stamp writes | Access allows |
|---|---|---|
profile-organization | The active profile's organization | That organization |
profile | The active profile id | That profile |
user | The login user id | That user |
domain-tenant | The tenant mapped to the domain's namespace | That 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
_tenantand overwrites anything the payload sent. - Update: the stored
_tenantis 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 shape | Stamp | Access |
|---|---|---|
| B2B SaaS, users act for an organization | profile-organization | profile-organization |
| Personal data, each user owns their rows | user | user |
| One tenant per deployment or per domain | domain-tenant | domain-tenant |
| Back-office portal over all tenants | (as the product) | system |
Related: Permissions tutorial · Configuration reference.