On a multi-tenant node every customer company is a tenant with its own database, registered in the control plane. This guide covers the operator commands that create a tenant, upgrade all tenants, change a membership, and end a tenant: server/provision_tenant.py, server/upgrade_tenants.py, server/membership.py, and server/offboard_tenant.py. The tenancy model itself is explained in multi-tenancy; self-service signup is in signup.
Prerequisites¶
- A node with
CLUSTER_ERP_MULTI_TENANT=trueand a complete environment file (src/cluster_erp/config/settings.py:82). See configuration reference. - A shell in the checkout as the service user, with the environment file loaded, as
install.shruns these commands (deploy/install.sh:63-66,deploy/install.sh:215-220). - For
--region: a region code listed inCLUSTER_ERP_REGIONS(src/cluster_erp/core/tenancy.py:456-457).
The states a tenant passes through are listed in TENANT_STATUSES: provisioning, active, suspended, and closed (src/cluster_erp/core/tenancy.py:48-50). The diagram shows the commands that move a tenant between them.
stateDiagram-v2 [*] --> provisioning: provision_tenant or signup provisioning --> active: provision completes active --> suspended: membership sweep or suspend suspended --> active: membership extend or activate active --> closed: offboard_tenant suspended --> closed: offboard_tenant closed --> [*]: offboard_tenant --drop
In prose: a tenant is registered as provisioning, becomes active when provisioning completes, moves to suspended when its paid period ends or an operator suspends it, returns to active on extension or activation, becomes closed on offboarding, and loses its database and files with the --drop step.
Steps¶
- Provision a tenant. Source:
src/cluster_erp/server/provision_tenant.py:3-5,src/cluster_erp/server/provision_tenant.py:163-172.
bash
python -m cluster_erp.server.provision_tenant --key <key> --name "<company name>" --domains <domain>,<domain> --region <region>
The key must match TENANT_KEY_RE (src/cluster_erp/core/tenancy.py:46, src/cluster_erp/core/tenancy.py:460-461). The command registers the tenant as provisioning, creates its database on the region's server, replays the Alembic history, runs the real application start-up once against the new database (tables, sequences, permissions, first administrator), and marks the tenant active (src/cluster_erp/server/provision_tenant.py:85-157). Each step is idempotent, so a failed run resumes when you run it again (src/cluster_erp/server/provision_tenant.py:7-15). Self-service signup calls the same function after the domain is verified (src/cluster_erp/server/signup.py:472-478, src/cluster_erp/server/signup.py:546).
- List what a release would change, before every upgrade. Source:
src/cluster_erp/server/upgrade_tenants.py:4,src/cluster_erp/server/upgrade_tenants.py:15-16.
bash
python -m cluster_erp.server.upgrade_tenants --dry-run
- Take a backup of every database, then upgrade all tenants.
install.shruns the dry run, the backup, and the upgrade in this order on a multi-tenant node (deploy/install.sh:217-220). See backups and restore.
bash
python -m cluster_erp.server.backup_tenants
python -m cluster_erp.server.upgrade_tenants
The runner visits the default database and every tenant whose registration is not deleted, suspended tenants included. It migrates a database that is behind the head, then runs the one-shot start-up against every tenant to refresh its permission catalogue and seeds. A failing tenant is reported and skipped (src/cluster_erp/server/upgrade_tenants.py:62-115).
-
If only one active tenant needs its seeds refreshed, run step 1 again with that tenant's key. An active tenant gets only the seed refresh (
src/cluster_erp/server/provision_tenant.py:114-123). The operator console starts the same refresh withPOST /api/v1/vendor/tenants/{key}/refresh-seeds(src/cluster_erp/server/routes/vendor.py:652-682). -
Extend, suspend, or reactivate a membership. Source:
src/cluster_erp/server/membership.py:3-13,src/cluster_erp/server/membership.py:156-171.
bash
python -m cluster_erp.server.membership extend --domain <domain> --days <days>
python -m cluster_erp.server.membership suspend --domain <domain>
python -m cluster_erp.server.membership activate --domain <domain>
extend moves the paid period forward and sets membership and tenant to active (src/cluster_erp/server/membership.py:83-106). Expired memberships are suspended automatically at start-up, daily, and by the cron sweep (src/cluster_erp/core/tenancy.py:324-345). See scheduled jobs and retention.
- Offboard a tenant: export its database and close the registration. Source:
src/cluster_erp/server/offboard_tenant.py:3-9.
bash
python -m cluster_erp.server.offboard_tenant --key <key> --note "<reason>"
The command writes an encrypted dump of the tenant database into backup_dir, then sets the tenant to closed, cancels its memberships, and stops sign-in resolution (src/cluster_erp/server/offboard_tenant.py:113-119, src/cluster_erp/core/tenancy.py:550-575).
- If the tenant's files and database must be removed, run the command again with
--dropand the key typed a second time. Source:src/cluster_erp/server/offboard_tenant.py:4,src/cluster_erp/server/offboard_tenant.py:11-16.
bash
python -m cluster_erp.server.offboard_tenant --key <key> --note "<reason>" --drop --confirm <key>
The command removes the tenant's folder from local storage, ends the database's sessions, drops the database, and appends the result to the tenant's closing note (src/cluster_erp/server/offboard_tenant.py:128-142). Earlier dumps stay until their own prune window, and the erasure ledger stays (src/cluster_erp/server/offboard_tenant.py:14-16).
Verify the result¶
provision_tenantprintstenant=<key> status=active db=<db> domains=<domains>(src/cluster_erp/server/provision_tenant.py:186-189).upgrade_tenantsprintsall tenant databases at headand exits with code 0 (src/cluster_erp/server/upgrade_tenants.py:128-132).python -m cluster_erp.server.membership listprints each tenant with its status and period end (src/cluster_erp/server/membership.py:53-81).- The operator console lists tenants with status, region, and schema revision; see operator console.
Troubleshooting¶
| Symptom | Cause in code | Fix |
|---|---|---|
tenant key ... must match ... |
The key breaks TENANT_KEY_RE (src/cluster_erp/core/tenancy.py:458-459) |
Use lowercase letters, digits, and underscores, starting with a letter. |
unknown region ... |
The region is not configured (src/cluster_erp/config/settings.py:917-923) |
Add the region to CLUSTER_ERP_REGIONS and its server to CLUSTER_ERP_REGION_DB_URLS. |
FAILED: <n> tenant(s) did not upgrade |
At least one tenant raised during upgrade (src/cluster_erp/server/upgrade_tenants.py:110-112, src/cluster_erp/server/upgrade_tenants.py:129-131) |
Read the logged exception of that tenant and run the runner again. |
offboarding is a multi-tenant operation; this node is single-tenant |
multi_tenant is off (src/cluster_erp/server/offboard_tenant.py:158-160) |
Run on a multi-tenant node. |
--drop requires --confirm <key> (typed again); nothing was done |
The confirmation is missing or differs (src/cluster_erp/server/offboard_tenant.py:108-110) |
Repeat the key after --confirm. |
storage backend ...: remove the tenant's objects with the provider's tooling |
The storage backend is not local (src/cluster_erp/server/offboard_tenant.py:65-73) |
Remove the objects outside this command. |
Discrepancies¶
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| tenant key | TENANT_KEY_RE |
The stable lowercase identifier of a tenant, also used as the suffix of its database name. | kiracı anahtarı | src/cluster_erp/core/tenancy.py:46 |
| seed refresh | _init_tenant_schema |
A one-shot application start-up against a tenant database that refreshes permissions, sequences, and deactivation columns. | tohum yenileme | src/cluster_erp/server/provision_tenant.py:39-82 |
| offboarding | offboard_tenant |
Ending a tenant: export, close, and optionally drop. | ayrılış | src/cluster_erp/server/offboard_tenant.py:1-16 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.