Skip to content

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).

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 anvil-tenant filter (part of every panel with tenancy) selects the tenant of each request:

  1. the tenant chosen in this session, as long as the user is still a member of it and the tenant is enabled,
  2. otherwise (after the login) the favourite tenant of the user,
  3. 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).

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); // ?int
service('tenantRepository')->toggleFavorite($userId, $tenantId); // members only

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(); // ?int
service('tenantContext')->require(); // int, throws without tenant

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.

Terminal window
php spark anvil:tenant:create "ACME GmbH" --admin jane@example.com
php spark anvil:tenant:add-member default jane@example.com --role Administrator

New 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.

  1. anvil:tenant:disable <slug>: the tenant is out of use, nothing is lost.
  2. Keep the data as long as contracts and retention periods require; export what must be kept.
  3. anvil:tenant:delete <slug> --confirm <slug>: deletes all records of the tenant (tables with a foreign key to anvil_tenants follow 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-users also 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.

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.

On (data kept without tenants):

Terminal window
php spark anvil:tenancy:enable --tenant "Main"
# then: Config\Anvil::$multiTenancy = true

All 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).

Terminal window
php spark anvil:tenancy:disable
# then: Config\Anvil::$multiTenancy = false

The roles of the tenant become global.