Multitenancy
View Sourcekura_tenant provides process dictionary-based multitenancy with two strategies: schema prefix and attribute-based. Once a tenant is set, all Kura repo operations in that process are automatically scoped.
Backend note: the schema-prefix strategy uses PostgreSQL schemas. SQLite has no schema concept; for multi-tenant SQLite use one database file per tenant or
ATTACH DATABASE. The attribute-based strategy works on any backend.
Strategies
Schema Prefix
Uses PostgreSQL schemas to isolate tenant data. Each tenant has its own schema (e.g., tenant_acme.users instead of public.users).
kura_tenant:put_tenant({prefix, ~"tenant_acme"}).
%% All queries now target the "tenant_acme" schema
{ok, Users} = kura_repo_worker:all(my_repo, kura_query:from(my_user)).
%% SELECT * FROM "tenant_acme"."users"Attribute-Based
Uses a shared table with a tenant identifier column. Queries automatically add a WHERE clause and inserts automatically include the tenant value.
kura_tenant:put_tenant({attribute, {org_id, 42}}).
%% Queries are scoped
{ok, Users} = kura_repo_worker:all(my_repo, kura_query:from(my_user)).
%% SELECT * FROM "users" WHERE "org_id" = 42
%% Inserts include the tenant attribute automatically
CS = kura_changeset:cast(my_user, #{}, #{~"name" => ~"Alice"}, [name]),
{ok, User} = kura_repo_worker:insert(my_repo, CS).
%% The inserted row has org_id = 42API
Setting a Tenant
kura_tenant:put_tenant({prefix, ~"tenant_acme"}).
kura_tenant:put_tenant({attribute, {org_id, 42}}).Returns the previously set tenant (or undefined).
Getting the Current Tenant
case kura_tenant:get_tenant() of
{prefix, Prefix} -> Prefix;
{attribute, {Field, Value}} -> {Field, Value};
undefined -> no_tenant
end.Clearing the Tenant
kura_tenant:clear_tenant().Returns the tenant that was cleared (or undefined).
Temporary Tenant Scope
with_tenant/2 sets a tenant for the duration of a function call, then restores the previous tenant:
kura_tenant:put_tenant({prefix, ~"tenant_acme"}).
Result = kura_tenant:with_tenant({prefix, ~"tenant_other"}, fun() ->
%% Inside here, tenant is "tenant_other"
kura_repo_worker:all(my_repo, kura_query:from(my_user))
end).
%% Tenant is back to "tenant_acme" hereThis is useful when you need to temporarily access a different tenant's data without affecting the surrounding context.
How Scoping Works
Tenant scoping is applied automatically by kura_repo_worker:
- Queries (
all,one,update_all,delete_all): the tenant filter is applied before the query is compiled to SQL. - Inserts: for attribute-based tenancy, the tenant field and value are merged into the changeset's changes before the INSERT is executed.
- Schema prefix: if the query already has an explicit prefix set, the tenant prefix is not overridden.
When to Use Which
Schema prefix is a good fit when:
- You need strong data isolation between tenants
- Each tenant may have schema-level customizations
- You are willing to manage per-tenant PostgreSQL schemas (migrations run per schema)
Attribute-based is a good fit when:
- You want simpler infrastructure (single schema, single set of migrations)
- Data isolation at the application level is sufficient
- You have many tenants and creating individual schemas is impractical
Usage in Web Applications
In a Nova handler or middleware, set the tenant early in the request lifecycle:
pre_request(Req, State) ->
TenantId = get_tenant_from_request(Req),
kura_tenant:put_tenant({attribute, {org_id, TenantId}}),
{ok, Req, State}.Since Cowboy spawns a process per request, the tenant is naturally isolated between concurrent requests. Remember to clear the tenant if the process is reused.