SaaS Tenant Suspension: Lifecycle Management for AI Platforms
TL;DR — SaaS tenant suspension manages the lifecycle of tenants who can't pay, violate policies, or choose to cancel. Microsoft Azure: "Deactivation differs from offboarding because it's intended to be a temporary state. However, after a period of time, you might choose to offboard a deactivated tenant." Six-state machine: trial → active → suspended → cancellation_scheduled → cancelled → deleted. Fremforge: "Suspended: write access is paused; read + export + reactivate still work. Cancelled: 60-day export + reactivate window. Deleted: Forgejo org and PII permanently destroyed, certificate of destruction issued." Acronis: "Disable restricts access for all user accounts. Delete is not reversible — all data deleted." Sollybombe: "Tenant lifecycle management isn't just about onboarding — it's about handling edge cases with precision." AI-specific: stop all agents, revoke API keys, purge queues, disconnect MCP. Integrate with multi-tenant architecture, role hierarchy, audit logging, and cost tracking.
Microsoft's multi-tenant guidance distinguishes the states: "Deactivation differs from offboarding because it's intended to be a temporary state. However, after a period of time, you might choose to offboard a deactivated tenant."
Fremforge's lifecycle docs implement a detailed state machine: "Suspended: write access is paused; read + export + reactivate still work. Cancelled: 60-day export + reactivate window. Deleted: org and PII permanently destroyed, certificate of destruction issued."
Tenant Lifecycle State Machine
Tenant States
| State | Trigger | Access | Data | Billing | Duration |
|---|---|---|---|---|---|
| Trial | Signup | Full (trial limits) | Stored | No charge | 30 days |
| Active | Paid plan | Full per plan | Stored | Charged | Indefinite |
| Suspended | Payment fail / admin action | Read-only (admins) | Preserved | Paused or continued | Until resolved or T+30 |
| Cancellation Scheduled | Customer cancels | Full until effective date | Stored | Until effective date | Until cancel_effective_at |
| Cancelled | Effective date reached | Read-only + export | Preserved (read-only) | Stopped | 60 days |
| Deleted | Retention expires | None | Permanently destroyed | Stopped | Irreversible |
Suspension Actions for AI Platforms
| Action | What Happens | Reversible |
|---|---|---|
| Stop agents | Force-stop all running agent executions | Yes (resume on reactivation) |
| Pause scheduled agents | Disable all cron and event-triggered agents | Yes |
| Revoke agent API keys | Invalidate all agent service account credentials | Yes (reissue on reactivation) |
| Disconnect MCP | Close all MCP server connections | Yes |
| Block LLM calls | Reject all LLM API calls for tenant | Yes |
| Block vector search | Reject all knowledge base queries | Yes |
| Stop webhooks | Stop processing incoming webhooks | Yes |
| Purge queues | Remove tenant's tasks from Redis/Kafka | No (tasks lost) |
| Terminate sessions | End all active user sessions | Yes (re-login on reactivation) |
| Freeze billing | Stop metering and billing (or continue per policy) | Yes |
Implementation
from enum import Enum
from datetime import datetime, timedelta, timezone
class TenantState(Enum):
TRIAL = "trial"
ACTIVE = "active"
SUSPENDED = "suspended"
CANCELLATION_SCHEDULED = "cancellation_scheduled"
CANCELLED = "cancelled"
DELETED = "deleted"
class TenantLifecycleManager:
"""Manages tenant suspension, reactivation, and offboarding."""
RETENTION_DAYS = 60 # Data retention after cancellation
DUNNING_DAYS = 14 # Days before suspension for payment failure
TRIAL_DAYS = 30 # Trial period
def __init__(self, db, redis, agent_manager, audit):
self.db = db
self.redis = redis
self.agents = agent_manager
self.audit = audit
async def suspend_tenant(
self, tenant_id: str, reason: str, suspended_by: str
) -> dict:
"""Suspend a tenant — pause write access, preserve data."""
tenant = await self.db.find_one("tenants", {"id": tenant_id})
if tenant["status"] != TenantState.ACTIVE.value:
raise InvalidStateError(
f"Cannot suspend tenant in {tenant['status']} state"
)
# 1. Update tenant status
await self.db.update("tenants", {"id": tenant_id}, {
"status": TenantState.SUSPENDED.value,
"suspended_at": datetime.now(timezone.utc),
"suspension_reason": reason,
"suspended_by": suspended_by,
})
# 2. Stop all AI agents
await self.agents.stop_all_agents(tenant_id)
# 3. Pause scheduled agents (cron, event-triggered)
await self.agents.pause_scheduled(tenant_id)
# 4. Revoke agent API keys
await self.db.update("agent_identities", {
"tenant_id": tenant_id,
"active": True,
}, {"active": False, "revoked_at": datetime.now(timezone.utc)})
# 5. Block API access (set flag in Redis for fast checks)
await self.redis.set(f"tenant_suspended:{tenant_id}", "1")
# 6. Purge queued tasks
await self._purge_tenant_queues(tenant_id)
# 7. Terminate active sessions
await self._terminate_sessions(tenant_id)
# 8. Audit log
await self.audit.log(
tenant_id=tenant_id,
user_id=suspended_by,
action="tenant_suspended",
details={"reason": reason},
)
# 9. Notify tenant admins
await self._notify_suspension(tenant_id, reason)
return {"tenant_id": tenant_id, "status": "suspended", "reason": reason}
async def reactivate_tenant(
self, tenant_id: str, reactivated_by: str
) -> dict:
"""Reactivate a suspended tenant — restore full access."""
tenant = await self.db.find_one("tenants", {"id": tenant_id})
if tenant["status"] not in (
TenantState.SUSPENDED.value, TenantState.CANCELLED.value
):
raise InvalidStateError(
f"Cannot reactivate tenant in {tenant['status']} state"
)
# 1. Update tenant status
await self.db.update("tenants", {"id": tenant_id}, {
"status": TenantState.ACTIVE.value,
"reactivated_at": datetime.now(timezone.utc),
"suspended_at": None,
"suspension_reason": None,
})
# 2. Remove suspension flag
await self.redis.delete(f"tenant_suspended:{tenant_id}")
# 3. Resume scheduled agents
await self.agents.resume_scheduled(tenant_id)
# 4. Reissue agent API keys
await self.db.update("agent_identities", {
"tenant_id": tenant_id,
"active": False,
}, {"active": True, "revoked_at": None})
# 5. Audit log
await self.audit.log(
tenant_id=tenant_id,
user_id=reactivated_by,
action="tenant_reactivated",
details={},
)
# 6. Notify tenant admins
await self._notify_reactivation(tenant_id)
return {"tenant_id": tenant_id, "status": "active"}
async def cancel_tenant(
self, tenant_id: str, cancelled_by: str, effective_date: datetime = None
) -> dict:
"""Schedule tenant cancellation — customer-initiated."""
if effective_date is None:
effective_date = datetime.now(timezone.utc) + timedelta(days=30)
await self.db.update("tenants", {"id": tenant_id}, {
"status": TenantState.CANCELLATION_SCHEDULED.value,
"cancel_effective_at": effective_date,
"cancelled_by": cancelled_by,
})
await self.audit.log(
tenant_id=tenant_id,
user_id=cancelled_by,
action="cancellation_scheduled",
details={"effective_date": effective_date.isoformat()},
)
return {
"tenant_id": tenant_id,
"status": "cancellation_scheduled",
"effective_date": effective_date.isoformat(),
"undo_available": True,
}
async def delete_tenant(self, tenant_id: str) -> dict:
"""Permanently delete tenant data — irreversible."""
tenant = await self.db.find_one("tenants", {"id": tenant_id})
retention_end = tenant.get("cancelled_at", datetime.now(timezone.utc))
if datetime.now(timezone.utc) < retention_end + timedelta(days=self.RETENTION_DAYS):
raise RetentionPeriodError("Retention period has not expired")
# 1. Emit audit event BEFORE deletion (while tenant scope is valid)
await self.audit.log(
tenant_id=tenant_id,
user_id="system",
action="tenant_physically_deleted",
details={},
)
# 2. Delete all tenant data
await self.db.delete_many("usage_records", {"tenant_id": tenant_id})
await self.db.delete_many("agent_identities", {"tenant_id": tenant_id})
await self.db.delete_many("knowledge_base", {"tenant_id": tenant_id})
await self.db.delete_many("user_roles", {"tenant_id": tenant_id})
await self.db.delete_many("audit_logs", {"tenant_id": tenant_id})
# 3. Null PII columns, set status
await self.db.update("tenants", {"id": tenant_id}, {
"status": TenantState.DELETED.value,
"deleted_at": datetime.now(timezone.utc),
"name": None,
"billing_email": None,
"display_name": None,
})
# 4. Purge Redis cache
keys = await self.redis.keys(f"*:{tenant_id}:*")
if keys:
await self.redis.delete(*keys)
return {
"tenant_id": tenant_id,
"status": "deleted",
"irreversible": True,
"certificate_of_destruction": True,
}
Dunning Workflow
| Day | Action | Email Sent | Access |
|---|---|---|---|
| 0 | Payment fails | "Payment failed" + update link | Full |
| 1 | Stripe retry 1 | — | Full |
| 4 | Stripe retry 2 | — | Full |
| 7 | Stripe retry 3 | "Action required: update payment" | Full |
| 11 | Stripe retry 4 | — | Full |
| 14 | Final retry fails | "Final notice: suspension imminent" | Full |
| 14+ | Suspend tenant | "Account suspended" | Read-only |
| 14+30 | Move to cancelled | "Account cancelled" | Read-only + export |
| 14+30+60 | Delete | "Data deleted" | None |
SaaS Tenant Suspension Checklist
- [ ] Implement tenant lifecycle state machine (trial, active, suspended, cancelled, deleted)
- [ ] Define state transitions and valid paths (monotonic except undo/reactivation)
- [ ] Set trial period (30 days) with auto-suspension on expiry
- [ ] Implement dunning workflow (14 days, 4 retries, 3 emails)
- [ ] Send warning emails before suspension (day 1, 7, 14)
- [ ] Never suspend without at least 7 days notice
- [ ] On suspension: stop all AI agents immediately
- [ ] On suspension: pause scheduled agents (cron, event-triggered)
- [ ] On suspension: revoke agent API keys
- [ ] On suspension: disconnect MCP server connections
- [ ] On suspension: block LLM API calls
- [ ] On suspension: block vector search / knowledge base queries
- [ ] On suspension: stop processing incoming webhooks
- [ ] On suspension: purge tenant's queued tasks from Redis/Kafka
- [ ] On suspension: terminate active user sessions
- [ ] On suspension: maintain read-only access for tenant admins
- [ ] On suspension: allow data export during suspension
- [ ] On suspension: allow self-reactivation via payment update
- [ ] Set Redis flag for fast suspension checks on every request
- [ ] Implement reactivation: resume agents, reissue keys, reconnect MCP
- [ ] Implement cancellation scheduling with undo window
- [ ] Set data retention period (60-90 days default, configurable per enterprise)
- [ ] Retain audit logs for full retention period
- [ ] On deletion: emit audit event BEFORE data removal
- [ ] On deletion: null PII columns, set status to deleted
- [ ] On deletion: purge all Redis caches for tenant
- [ ] On deletion: purge backups at +120 days
- [ ] Issue certificate of destruction for enterprise tenants
- [ ] Support GDPR right to erasure: tenant deletion removes all org-scoped data
- [ ] Integrate with multi-tenant architecture isolation
- [ ] Integrate with role hierarchy for access revocation
- [ ] Log all lifecycle transitions in audit logging
- [ ] Integrate with cost tracking to stop billing
- [ ] Integrate with usage metering to stop metering
- [ ] Apply zero-trust architecture during suspension
- [ ] Set up platform monitoring for lifecycle events
- [ ] Consider white-label branded suspension emails
- [ ] Consider BYOK key revocation on suspension
- [ ] Test suspension: verify all access is blocked
- [ ] Test reactivation: verify all access is restored
- [ ] Test deletion: verify all data is permanently removed
- [ ] Test dunning: verify email sequence and timing
- [ ] Document lifecycle states and transitions for compliance audits
- [ ] Quarterly review: audit suspended/cancelled tenants, clean up expired data
- [ ] Consider AI incident response for suspension failures
FAQ
What is SaaS tenant suspension?
SaaS tenant suspension is the process of temporarily disabling a tenant's access to the platform while preserving their data for potential reactivation. Suspension is triggered by: (1) Payment failure (dunning past T+14 days). (2) Trial expiration (trial ended without conversion). (3) Admin action (policy violation, security concern). (4) Plan downgrade with data exceeding new plan limits. During suspension: write access is paused, read access and data export remain available for org admins, agents stop running, API access is revoked, and billing continues (or pauses depending on policy). Suspension is temporary — the tenant can reactivate by paying outstanding invoices or resolving the issue. This differs from offboarding (permanent) and deletion (irreversible).
What is the tenant lifecycle state machine?
The tenant lifecycle state machine has six states: (1) Trial — first 30 days, no card required. Auto-suspends if not converted by trial_expires_at. (2) Active — paid plan running, normal operation. (3) Suspended — payment failure or admin action. Write access paused, read + export + reactivate available. (4) Cancellation Scheduled — customer initiated cancel, service continues until cancel_effective_at. Undo lifts back to active. (5) Cancelled — cancel_effective_at reached, data read-only, 60-day export + reactivate window. (6) Deleted — retention period expired, all data permanently destroyed, certificate of destruction issued. The state machine is monotonic except for cancellation_scheduled → active (undo) and suspended → active (reactivation).
How long should suspended tenant data be retained?
Data retention for suspended/cancelled tenants depends on: (1) Legal requirements — GDPR, HIPAA, or industry regulations may mandate specific retention or destruction periods. (2) Enterprise contracts — custom retention periods negotiated per tenant (e.g., 90 days, 180 days, 1 year). (3) Default policy — typically 60-90 days for standard plans. (4) Audit trail — audit logs retained for the full retention period, even after data deletion. Implementation: Active tenants retain data indefinitely. Suspended tenants retain data for the suspension period. Cancelled tenants retain data for 60-90 days (configurable). After retention: all tenant data permanently deleted (irreversible), PII columns nulled, backups purged at +120 days. Issue a certificate of destruction for enterprise tenants.
What happens to AI agents when a tenant is suspended?
When a tenant is suspended, all AI agents must stop immediately: (1) Running agents — force-stop all in-flight agent executions. (2) Scheduled agents — pause all cron and event-triggered agents. (3) Agent API keys — revoke all agent service account credentials. (4) MCP connections — disconnect all MCP server connections. (5) LLM calls — block all LLM API calls for the tenant. (6) Vector search — block all knowledge base queries. (7) Webhooks — stop processing incoming webhooks for the tenant. (8) Queued tasks — purge tenant's tasks from Redis/Kafka queues. (9) Sessions — terminate all active user sessions. (10) Audit — log the suspension with reason, timestamp, and admin who triggered it. On reactivation: resume scheduled agents, reissue API keys, reconnect MCP, resume webhook processing.
How do you handle dunning and payment failure suspension?
Dunning is the process of handling failed payments before suspension. Workflow: (1) Payment fails — retry automatically (Stripe Smart Retries, up to 4 attempts over 14 days). (2) Day 1 — email customer about failed payment with update link. (3) Day 7 — second email, warn about upcoming suspension. (4) Day 14 — final notice, suspension imminent. (5) Day 14+ — suspend tenant if payment still failed. Write access paused, read access maintained. (6) During suspension — customer can still update payment method and self-reactivate. (7) Day 14+30 — if still unpaid, move to cancellation_scheduled. (8) Day 14+30+60 — permanent deletion. Keep dunning emails branded (per reseller for white-label). Never suspend without warning — always give at least 7 days notice.
Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →