Developer Docs

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:

  1. Marks the BillingSubscription as CANCELED
  2. Calls updateOrgStatus(orgId, 'SUSPENDED') via HTTP to the organizations subgraph using the x-internal-caller: payments header
  3. Emits a subscription_expired event 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:

  1. The cron job transitions the org to FROZEN via updateOrgStatus
  2. It calls suspendOrgMalets(orgId) on the Malets Subgraph
  3. The Malets subgraph iterates through all Malets owned by the org, setting isSuspended: true and suspendedAt: Date.now(). This effectively unpublishes all storefronts and APIs.
  4. Emits SUBSCRIPTION_ORG_FROZEN to alerts notifying the owner.

Day 60: Archival Warning

If an organization remains FROZEN for 60 days (90 days since cancellation):

  1. The cron job queries for subscriptions canceled 60+ days ago where dataRetentionWarningSentAt is null.
  2. It emits a SUBSCRIPTION_DATA_RETENTION_WARNING event to alerts stating the organization will be archived in 30 days.
  3. Sets dataRetentionWarningSentAt to 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:

  1. Provisions the new BillingSubscription record.
  2. Calls updateOrgStatus(orgId, 'ACTIVE') via HTTP.
  3. If the org was FROZEN, it calls restoreOrgMalets(orgId) on the Malets Subgraph, which clears the isSuspended and suspendedAt flags, republishing the storefronts exactly as they were.
  4. Emits a SUBSCRIPTION_RESTORED event 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