Tenancy
A tenant is a separate customer or organisation unit: its records, roles, files, notifications, tasks and workflow configurations are isolated from the other tenants (see data rules).
Users in several tenants
Section titled “Users in several tenants”A user can belong to any number of tenants and has roles per tenant: an administrator in tenant A can be a reader in tenant B or have no access there. Permissions are always those of the active tenant.
Single sign-on users get their memberships from their groups: map a group to a role in every tenant (role editor), and the user becomes a member of all of them at the next login.
The active tenant
Section titled “The active tenant”The anvil-tenant filter (part of every panel with tenancy) selects the
tenant of each request:
- the tenant chosen in this session, as long as the user is still a member of it and the tenant is enabled,
- otherwise (after the login) the favourite tenant of the user,
- otherwise the first of their tenants by name.
Users without any enabled tenant get a “no tenant” page (HTTP 403).
The topbar shows the active tenant. Users with two or more tenants switch
there; the switch is a POST to /<panel>/_tenant with a token bound to the
session, only tenants of the user are accepted, and it is written to the
audit log (tenant.switched; refused attempts as security event
tenant.switch_denied).
Favourite tenant
Section titled “Favourite tenant”The star next to a tenant in the switcher makes it the favourite: it is
selected after the next login. A second click removes it; a user has at
most one favourite. It is a personal preference, stored in
anvil_tenant_users.is_favorite, without audit entry.
service('tenantRepository')->favoriteId($userId); // ?intservice('tenantRepository')->toggleFavorite($userId, $tenantId); // members onlyIn jobs and commands
Section titled “In jobs and commands”Outside of a request no tenant is set. Run tenant scoped code inside the tenant; the previous tenant is restored afterwards:
service('tenantContext')->runAs($tenantId, function (): void { // models, notifications, tasks and workflows work in this tenant});
service('tenantContext')->id(); // ?intservice('tenantContext')->require(); // int, throws without tenantAdministration (command line)
Section titled “Administration (command line)”Tenants are managed by the operators of the application on the command line. There is no web access to these operations: a taken over web account cannot create, change or delete tenants. Every command is written to the audit log (with the operating system user).
| Command | Effect |
|---|---|
anvil:tenant:list |
Tenants with status and number of members |
anvil:tenant:create "<name>" [--slug <slug>] [--admin <e-mail>] |
Creates a tenant; --admin makes an existing user its administrator (role “Administrator”, full access) |
anvil:tenant:rename <slug> "<name>" |
Renames a tenant |
anvil:tenant:disable <slug>, anvil:tenant:enable <slug> |
A disabled tenant cannot be selected; its users work in their other tenants, users of this tenant only are rejected |
anvil:tenant:delete <slug> --confirm <slug> [--with-users] |
Deletes a disabled tenant with all its data |
anvil:tenant:add-member <slug> <e-mail> [--role <name>] |
Adds an existing user, optionally with a role of the tenant (“Administrator” is created with full access if missing) |
anvil:tenant:remove-member <slug> <e-mail> |
Removes the user and their roles from the tenant; the account stays |
Example: an administrator of two tenants.
php spark anvil:tenant:create "ACME GmbH" --admin jane@example.comphp spark anvil:tenant:add-member default jane@example.com --role AdministratorNew accounts are created in the user administration of a tenant (or by single sign-on); accounts that exist already join further tenants on the command line. Tenant administrators therefore cannot find out which e-mail addresses exist in other tenants.
The same operations are available in code as
service('tenantAdministration') (all(), find($slug), create(),
rename(), setActive(), delete(), addMember(), removeMember()),
e.g. for seeders; they are audited the same way.
Deleting
Section titled “Deleting”anvil:tenant:disable <slug>: the tenant is out of use, nothing is lost.- Keep the data as long as contracts and retention periods require; export what must be kept.
anvil:tenant:delete <slug> --confirm <slug>: deletes all records of the tenant (tables with a foreign key toanvil_tenantsfollow it, tables with a tenant column without such a key are cleared as well), its roles, memberships, saved views, notifications, exports and stored files.--with-usersalso deletes accounts that belong to no other tenant.
The audit log is kept: its entries remain evidence and the hash chain stays valid. Deleting cannot be undone.
Tables of your application that reference tenant data without a tenant
column (e.g. pivot tables created with --no-tenant) need foreign keys
with ON DELETE CASCADE, otherwise their rows remain.
Without tenants
Section titled “Without tenants”Config\Anvil::$multiTenancy = false runs the application without tenants:
models are not scoped, roles are global, there is no tenant switcher and
the anvil-tenant filter does nothing. A single panel can opt out with
hasTenancy(), see Tenants.
Recommendation: keep multi tenancy on, also with a single tenant
(anvil:install creates “Default”). Adding a second tenant later is then one
command. Switch it off only for applications that will never have tenants.
The generators create a tenant_id column in any case (unless --no-tenant),
so switching later needs no schema change.
Switching later
Section titled “Switching later”On (data kept without tenants):
php spark anvil:tenancy:enable --tenant "Main"# then: Config\Anvil::$multiTenancy = trueAll records, roles and users are assigned to the new tenant. The command lists tables of Anvil models without a tenant column; add one with a migration if their records belong to tenants.
Off: only with a single tenant, otherwise the data of all tenants would be visible to all users (the command refuses).
php spark anvil:tenancy:disable# then: Config\Anvil::$multiTenancy = falseThe roles of the tenant become global.