Back to Skills
💼
VerifiedSimple🥈silver💼Business Operations

Subscription Lifecycle Manager

Handle subscription state changes, tier migrations, cancellations, and edge cases in the billing lifecycle.

Verified
Version1.0.0
AuthorID8Labs
LicenseMIT
Published1/6/2026
View on GitHub

Skill Content

# Subscription Lifecycle Manager Skill

Handle subscription state changes, tier migrations, cancellations, and edge cases in the billing lifecycle.

## When to Use This Skill

Use when dealing with:
- User upgrades (FREE → PRO/ENTERPRISE)
- User downgrades (PRO → FREE)
- Cancellations
- Payment failures and recovery
- Trial periods
- Subscription pauses/resumes

## Subscription State Machine

```
          ┌─────────┐
          │  NONE   │ (no subscription)
          └────┬────┘
               │
               │ signup/free tier
               ↓
          ┌─────────┐
          │  FREE   │
          └────┬────┘
               │
               │ upgrade checkout
               ↓
        ┌──────────────┐
        │  TRIALING    │ (if trial enabled)
        └──────┬───────┘
               │
               │ trial ends + payment succeeds
               ↓
          ┌─────────┐
          │ ACTIVE  │ (PRO/ENTERPRISE)
          └────┬────┘
               │
               ├──────→ (payment fails) → ┌────────────┐
               │                           │  PAST_DUE  │
               │                           └─────┬──────┘
               │                                 │
               │   ←───(payment recovered)───────┘
               │
               ├──────→ (user cancels) ──→ ┌──────────────────┐
               │                            │ CANCEL_AT_PERIOD │
               │                            │      _END        │
               │                            └────────┬─────────┘
               │                                     │
               │   ←──(user reactivates)─────────────┘
               │
               ↓ (period ends if cancelled)
          ┌─────────┐
          │ CANCELED│
          └────┬────┘
               │
               │ downgrade to FREE
               ↓
          ┌─────────┐
          │  FREE   │
          └─────────┘
```

## Common Lifecycle Scenarios

### 1. Upgrade: FREE → PRO

**Trigger**: User clicks "Upgrade to Pro"

**Flow**:
1. User clicks upgrade button
2. POST /api/billing/checkout with `priceId` for PRO tier
3. Redirect to Stripe checkout
4. User completes payment
5. Webhook: `checkout.session.completed`
6. `syncSubscriptionToDatabase()` → creates subscription record
7. `tier` = PRO, `status` = active
8. User refreshed → sees PRO features

**Edge Cases**:
- User already has active subscription → Block checkout, show "Manage Subscription"
- User had PRO before, cancelled → Allow re-upgrade (new subscription)
- Payment fails during checkout → No subscription created, user stays FREE

### 2. Downgrade: PRO → FREE

**Trigger**: User cancels in Stripe billing portal

**Flow**:
1. User clicks "Manage Subscription" → opens Stripe portal
2. User clicks "Cancel subscription"
3. Stripe sets `cancel_at_period_end = true`
4. Webhook: `customer.subscription.updated`
5. Database updated: `cancel_at_period_end = true`
6. User continues using PRO until period ends
7. Period ends → Webhook: `customer.subscription.deleted`
8. `downgradeTier()` → sets tier = FREE
9. User now subject to FREE limits

**Edge Cases**:
- User cancels then reactivates before period ends → Keep PRO, set `cancel_at_period_end = false`
- User has 3 active projects but FREE allows 1 → Need grace period or project archive flow
- User in middle of AI generation when downgraded → Allow that generation to complete

### 3. Payment Failure → Recovery

**Trigger**: Credit card declines

**Flow**:
1. Stripe retries payment automatically (configurable)
2. Webhook: `invoice.payment_failed`
3. `markSubscriptionPastDue()` → status = past_due
4. Email sent to user: "Update payment method"
5. Banner shown in app: "Payment failed"
6. User updates card in Stripe portal
7. Stripe retries payment → succeeds
8. Webhook: `invoice.payment_succeeded`
9. `updateBillingPeriod()` → status = active
10. Remove banner, send confirmation email

**Edge Cases**:
- Payment fails 4 times → Stripe cancels subscription
- User ignores emails → After X days, downgrade to FREE
- User disputes charge → Stripe notifies via webhook, handle separately

### 4. Trial Period (if enabled)

**Trigger**: User upgrades with trial

**Flow**:
1. Checkout session created with `trial_period_days = 14`
2. Subscription created with status = trialing
3. User has full PRO access
4. 3 days before trial ends → Webhook: `customer.subscription.trial_will_end`
5. Email sent: "Trial ending soon, card will be charged $19"
6. Trial ends → Stripe charges card
7. If payment succeeds → status = active
8. If payment fails → status = past_due → see payment failure flow

**Edge Cases**:
- User cancels during trial → No charge, downgrade to FREE
- User already had trial before → Don't offer trial again (track in metadata)

### 5. Subscription Pausing (future feature)

**Trigger**: User requests to pause (e.g., vacation)

**Flow**:
1. User clicks "Pause subscription" in billing portal
2. POST /api/billing/pause
3. Stripe API: `stripe.subscriptions.update(id, { pause_collection: { behavior: 'mark_uncollectible' } })`
4. Webhook: `customer.subscription.updated`
5. Database: status = paused
6. User downgraded to FREE during pause
7. User clicks "Resume" → status = active, tier = PRO

## Helper Functions

**Check if upgrade is allowed**:
```typescript
async function canUpgrade(userId: string): Promise<{ allowed: boolean; reason?: string }> {
  const sub = await checkUserSubscription(userId);

  // No active subscription → can upgrade
  if (!sub || sub.status !== 'active') {
    return { allowed: true };
  }

  // Already on paid tier
  if (sub.tier !== 'FREE') {
    return {
      allowed: false,
      reason: 'You already have an active subscription. Use "Manage Subscription" to change plans.'
    };
  }

  return { allowed: true };
}
```

**Calculate prorated amount**:
```typescript
function calculateProration(
  currentTier: string,
  newTier: string,
  daysRemaining: number
): number {
  // Stripe handles this automatically, but useful for display
  const currentPrice = PRICING_PLANS[currentTier].monthlyPrice;
  const newPrice = PRICING_PLANS[newTier].monthlyPrice;

  const dailyRate = newPrice / 30;
  const credit = (currentPrice / 30) * daysRemaining;

  return Math.max(0, (dailyRate * daysRemaining) - credit);
}
```

**Handle grace period for downgrade**:
```typescript
async function handleDowngradeGracePeriod(userId: string) {
  // Give user 7 days to reduce projects before enforcing limits
  const gracePeriodEnd = new Date();
  gracePeriodEnd.setDate(gracePeriodEnd.getDate() + 7);

  await createNotification(userId, {
    type: 'downgrade_grace_period',
    message: `You have 7 days to reduce your projects to 1 before FREE tier limits are enforced.`,
    expiresAt: gracePeriodEnd,
  });

  // Schedule job to enforce limits after grace period
  // (implement with cron or background job)
}
```

## Best Practices

1. **Always use Stripe as source of truth**: If database and Stripe disagree, trust Stripe
2. **Idempotency**: Webhooks may fire multiple times - handle gracefully
3. **Graceful degradation**: If subscription sync fails, don't block user
4. **Clear communication**: Email user about every state change
5. **Undo window**: Allow short period to undo cancellations
6. **Data retention**: Keep cancelled subscription records for analytics

## Conclusion

Subscription lifecycle is complex. Test every transition thoroughly, especially edge cases where users might lose access to features or data.

Tags

Statistics

Installs0
Views0

Related Skills