When a Pro or Enterprise subscription is cancelled or payment fails, the platform triggers a notification pipeline and tracks the organization's operational status. This document covers the full lifecycle from cancellation scheduling through org suspension.
Organization Status
The Organization entity now includes an OrgStatus enum that tracks operational state:
| Status | Meaning | Trigger |
|---|---|---|
ACTIVE |
Fully operational (default) | Org creation, re-subscription |
SUSPENDED |
Read-only โ no new content creation | Subscription expires (customer.subscription.deleted) |
FROZEN |
All Malets unpublished | 30 days post-expiry |
ARCHIVED |
Final dormant state, data preserved but locked | 90 days post-expiry |
The status field is a @FilterableField on the Organization entity, stored in MongoDB with type: String, enum: OrgStatus, default: ACTIVE. All existing organizations default to ACTIVE without a migration.
Cancellation Flow
When a user cancels their subscription from the billing page, Stripe sets cancel_at_period_end: true on the subscription. The platform handles this in three stages:
1. Cancellation Scheduled
Stripe event: customer.subscription.updated with cancel_at_period_end: true
The payments subgraph detects this in handleSubscriptionUpdated and emits a subscription_cancellation_scheduled event to the alerts subgraph:
alertsClient.emit('subscription_cancellation_scheduled', {
orgId: subscription.orgId,
userId: subscription.userId,
cancelAt: '2026-06-11T00:00:00.000Z',
planName: 'Pro',
currentPeriodEnd: '2026-06-11T00:00:00.000Z',
});
The alerts subgraph delivers a notification: "Your subscription will end on {date}. Your org and Malets will enter read-only mode."
The cancelAtPeriodEnd boolean is persisted on the BillingSubscription model so the Deck can display a cancellation banner in the org dashboard.
2. Payment Failure
Stripe event: invoice.payment_failed
When a recurring invoice payment fails (card declined, expired, etc.), the webhook controller routes to handleInvoicePaymentFailed, which emits a subscription_payment_failed event:
alertsClient.emit('subscription_payment_failed', {
orgId: subscription.orgId,
userId: subscription.userId,
invoiceId: 'inv_xxx',
amountDue: 2900, // cents
attemptCount: 1, // Stripe retries 3 times by default
nextAttempt: '2026-05-14T00:00:00.000Z',
});
After 3 failed retries (Stripe's default Smart Retries), the subscription transitions to UNPAID, then Stripe fires customer.subscription.deleted.
3. Subscription Expired (Grace Period)
Stripe event: customer.subscription.deleted
The handleSubscriptionDeleted handler in the webhook service:
- Marks the
BillingSubscriptionasCANCELED - Calls
updateOrgStatus(orgId, 'SUSPENDED')via HTTP to the organizations subgraph using thex-internal-caller: paymentsheader - Emits a
subscription_expiredevent to alerts
The organization is now in read-only mode (Grace Period) โ members can view data but cannot create or edit products, services, or blog posts. This is enforced globally by the OrgStatusGuard.
Automated Enforcement Lifecycle
Once an organization is SUSPENDED, the SubscriptionLifecycleService in the payments subgraph takes over. This service runs a daily Cron job (@Cron(CronExpression.EVERY_DAY_AT_3AM)) to enforce the long-term expiration lifecycle.
Day 30: Freeze & Unpublish
If an organization remains SUSPENDED for 30 days without re-subscribing:
- The cron job transitions the org to
FROZENviaupdateOrgStatus - It calls
suspendOrgMalets(orgId)on the Malets Subgraph - The Malets subgraph iterates through all Malets owned by the org, setting
isSuspended: trueandsuspendedAt: Date.now(). This effectively unpublishes all storefronts and APIs. - Emits
SUBSCRIPTION_ORG_FROZENto alerts notifying the owner.
Day 60: Archival Warning
If an organization remains FROZEN for 60 days (90 days since cancellation):
- The cron job queries for subscriptions canceled 60+ days ago where
dataRetentionWarningSentAtis null. - It emits a
SUBSCRIPTION_DATA_RETENTION_WARNINGevent to alerts stating the organization will be archived in 30 days. - Sets
dataRetentionWarningSentAtto prevent duplicate warnings.
Day 90: Organization Archival
At 90 days FROZEN (120 days since cancellation), the organization transitions to ARCHIVED. This is a final dormant state where all data is preserved indefinitely but the organization is completely locked down.
Re-subscription & Recovery
If an organization owner upgrades or creates a new subscription at any point during the SUSPENDED, FROZEN, or ARCHIVED periods:
Stripe event: customer.subscription.created
The handleSubscriptionCreated webhook automatically recovers the organization:
- Provisions the new
BillingSubscriptionrecord. - Calls
updateOrgStatus(orgId, 'ACTIVE')via HTTP. - If the org was
FROZEN, it callsrestoreOrgMalets(orgId)on the Malets Subgraph, which clears theisSuspendedandsuspendedAtflags, republishing the storefronts exactly as they were. - Emits a
SUBSCRIPTION_RESTOREDevent to alerts welcoming them back.
Cross-Subgraph Communication
The org status and malet suspension updates use the internal-caller HTTP pattern established in the Malet-First Ownership Architecture for atomic operations:
payments โ HTTP POST โ organizations/graphql
Header: x-internal-caller: payments
Mutation: updateOrgStatus(input: { orgId, status: FROZEN })
payments โ HTTP POST โ malets/graphql
Header: x-internal-caller: payments
Mutation: suspendOrgMalets(orgId: "...")
These mutations are protected by InternalCallerGuard and are inaccessible from the public API. Every state change is recorded in the audit log with actorId: 'system:payments'.
Event Constants
All lifecycle events are defined in libs/common/src/constants/index.ts:
| Constant | Event Name | Trigger |
|---|---|---|
SUBSCRIPTION_CANCELLATION_SCHEDULED |
subscription_cancellation_scheduled |
cancel_at_period_end: true |
SUBSCRIPTION_PAYMENT_FAILED |
subscription_payment_failed |
invoice.payment_failed |
SUBSCRIPTION_EXPIRED |
subscription_expired |
customer.subscription.deleted |
SUBSCRIPTION_ORG_FROZEN |
subscription_org_frozen |
30 days post-expiry |
SUBSCRIPTION_DATA_RETENTION_WARNING |
subscription_data_retention_warning |
60 days post-freeze |
SUBSCRIPTION_RESTORED |
subscription_restored |
Re-subscription webhook |
Environment Variables
| Variable | Service | Description |
|---|---|---|
ORGANIZATIONS_SERVICE_URL |
payments | HTTP endpoint for cross-subgraph org status updates |
STRIPE_SECRET_KEY |
payments | Stripe API key for subscription management |
STRIPE_WEBHOOK_SECRET |
payments | Webhook signature verification |
Related
- Subscription & Billing Architecture โ Tier enforcement, single sources of truth, and fail-open strategy
- Payments & Subscriptions โ Full API reference for the payments subgraph
- Organizations & Permissions โ The Organization entity and membership model
- Alerts & Notifications โ The delivery infrastructure for lifecycle notifications
- User Guide: Managing Your Subscription โ End-user guide to cancellation and resubscription