Tenant lifecycle
Every tenant moves through a small, explicit set of states. Stratum models the lifecycle as a strict state machine so each transition is a named operation with predictable behavior, both for the tenant itself and for its descendants.
States
Section titled “States”| State | Meaning | Reversible |
|---|---|---|
pending |
Created with its own schema or database, which is not yet provisioned. Not usable. | Becomes active via activateTenant |
active |
Normal, fully usable tenant. | n/a |
suspended |
Access is blocked, but the tenant and its data are intact. | Yes, via resumeTenant |
archived |
Soft-deleted: hidden from listings, deleted_at set, data retained. |
Yes, via resumeTenant |
| (purged) | Hard-deleted for GDPR erasure. The row and all related data are gone. | No |
Transitions
Section titled “Transitions” createTenant │ ▼ suspendTenant ┌────────┐ archiveTenant ┌─────────│ active │─────────┐ ▼ └────────┘ ▼ ┌───────────┐ ▲ ┌──────────┐ │ suspended │ │ │ archived │ └───────────┘ resumeTenant └──────────┘ │ │ │ └──────────────┴──────────────┘ │ purgeTenant ──▶ (row gone, irreversible)| Operation | From | To | Notes |
|---|---|---|---|
createTenant |
none | active, or pending for SCHEMA_PER_TENANT / DB_PER_TENANT |
Parent must be active. |
activateTenant |
pending |
active |
Call once the tenant’s schema or database exists. Parent must be active. |
suspendTenant |
active |
suspended |
Blocks access; reversible. |
resumeTenant |
suspended, archived |
active |
Reverses suspend and archive; clears deleted_at. |
archiveTenant |
active, suspended |
archived |
Soft delete; reversible. |
purgeTenant |
any | (gone) | GDPR Article 17 hard delete; irreversible. |
deleteTenant is retained as a deprecated alias of archiveTenant: soft
deleting a tenant is archiving it.
Blocked access
Section titled “Blocked access”A pending, suspended or archived tenant is excluded from listTenants,
getChildren, and getDescendants, its API keys do not authenticate, and
getTenant throws:
TenantPendingError(409) for a pending tenantTenantSuspendedError(403) for a suspended tenantTenantArchivedError(410) for an archived tenant
listTenants(pagination, { status: "pending" }) (or GET /api/v1/tenants?status=pending)
lists tenants in another state.
Pass includeArchived: true to getTenant or getDescendants when you
genuinely need the row regardless of state (for lifecycle or retention passes):
await stratum.suspendTenant(tenant.id);
await stratum.getTenant(tenant.id); // throws TenantSuspendedErrorawait stratum.getTenant(tenant.id, true); // returns the suspended row
await stratum.resumeTenant(tenant.id); // back to activeDescendant rules
Section titled “Descendant rules”The interesting cases are transitions on a tenant that has a subtree. The rules keep one invariant true at all times: an active tenant’s parent is always active.
- Downward transitions block, they do not cascade.
suspendTenantandarchiveTenantreject withTenantHasChildrenError(409) if the tenant has anyactivechild. Work leaf-first: suspend or archive the children before the parent. - Upward transitions are top-down.
resumeTenant,activateTenant,createTenant,batchCreateTenantsandmoveTenantrequire the (new) parent to beactive. You cannot resume a tenant whose parent is still suspended or archived, nor create or move a child under a non-active parent. Resume the parent first. - Purge requires an empty subtree.
purgeTenantrejects while the tenant has any child of any status. Purge from the leaves up.
Tenants with their own storage
Section titled “Tenants with their own storage”A SCHEMA_PER_TENANT or DB_PER_TENANT tenant is created pending. The
control plane then provisions its schema or database and activates it, so
POST /api/v1/tenants returns an active tenant. When you use the library
directly, provision the storage yourself and then call activateTenant.
If provisioning fails, the control plane returns TENANT_PROVISIONING_FAILED
with the tenant id and leaves the tenant pending. To retry, purge that tenant
and create it again. Purging a tenant that is still pending removes only its
rows and never drops a schema or database, because its storage was never
confirmed as its own. A tenant-scoped API key with the admin scope can purge a
pending tenant in its own subtree. You do not need an operator key for it.
If provisioning succeeds but activation fails, the control plane drops the
schema or database it just created. It then returns TENANT_PROVISIONING_FAILED
with details.stage set to "activation", and leaves the tenant pending.
Purge the tenant and create it again, as above. If details.storage_removed is
false, the drop also failed: drop that schema or database by hand before you
create the tenant again. For a failure during provisioning, details.stage is
"provisioning".
The control plane drops the storage only when it can confirm that the tenant is
still pending. If it cannot read the tenant after the activation error, it
keeps the schema or database and returns the original error. In that case, get
the tenant with GET /api/v1/tenants/:id. If the response is the tenant, it is
active and ready to use. If the response is TENANT_PENDING, purge the tenant,
drop its schema or database by hand, and create it again.
// Blocked: root still has an active child.await stratum.suspendTenant(root.id); // throws TenantHasChildrenError
// Leaf-first works.await stratum.suspendTenant(child.id);await stratum.suspendTenant(root.id); // ok
// Resume is top-down.await stratum.resumeTenant(child.id); // throws while root is suspendedawait stratum.resumeTenant(root.id);await stratum.resumeTenant(child.id); // okWebhook events
Section titled “Webhook events”Each lifecycle transition emits an event you can subscribe a webhook to:
tenant.activated, tenant.suspended, tenant.resumed, and tenant.archived
(plus tenant.deleted from the deleteTenant alias). See
Webhooks.
A purge emits no webhook event. The purge erases the event log of the tenant with the tenant itself.
tenant.purged is deprecated: purgeTenant never emits it, and the next major version removes it.