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 behaviour, both for the tenant itself and for its descendants.
States
Section titled “States”| State | Meaning | Reversible |
|---|---|---|
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 |
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 suspended or archived tenant is excluded from listTenants,
getChildren, and getDescendants, and getTenant throws:
TenantSuspendedError(403) for a suspended tenantTenantArchivedError(410) for an archived tenant
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.
resumeTenantandcreateTenantrequire the parent to beactive. You cannot resume a tenant whose parent is still suspended or archived, nor create 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.
// 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.suspended, tenant.resumed, tenant.archived, and tenant.purged
(plus tenant.deleted from the deleteTenant alias). See
Webhooks.