Agent skill
subscription-patterns
Subscription lifecycle patterns for Stripe + Convex/database backends. Invoke for: trial-to-paid flows, subscription state management, webhook handling, access control logic, cancel/resubscribe edge cases.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/subscription-patterns
SKILL.md
Subscription Lifecycle Patterns
Best practices for managing subscription states, trials, and access control with Stripe.
Core Principle
Stripe is the source of truth for billing. Your database caches state for access decisions.
Trial-to-Paid Flow
The Right Way: Use Stripe's trial_end
When a user subscribes during their trial, honor remaining days:
// In checkout session creation
const TRIAL_DURATION_MS = 14 * 24 * 60 * 60 * 1000;
// Calculate remaining trial
const trialEndMs = user?.trialEndsAt
?? (user?._creationTime ? user._creationTime + TRIAL_DURATION_MS : null);
const now = Date.now();
const hasRemainingTrial = trialEndMs && trialEndMs > now;
const trialEndSeconds = hasRemainingTrial ? Math.floor(trialEndMs / 1000) : undefined;
// Pass to Stripe - it handles billing delay
const session = await stripe.checkout.sessions.create({
// ...
subscription_data: {
metadata: { userId },
...(trialEndSeconds && { trial_end: trialEndSeconds }),
},
});
Benefits:
- Stripe delays first charge until
trial_end - User sees "trial ends on X" in Stripe customer portal
- No manual billing logic needed
Prevent Zombie Trials
Problem: If trial data persists after subscription, canceled users may regain access.
Solution: Clear trial when subscription activates:
// In webhook handler / updateFromStripe mutation
await db.patch(user._id, {
subscriptionStatus: status,
// Clear trial to prevent zombie access after cancel
...(status === "active" && { trialEndsAt: 0 }),
});
Access Control Priority
Check states in this order (first match wins):
function hasAccess(user): boolean {
// 1. Active subscription - always grants access
if (user.subscriptionStatus === "active") return true;
// 2. Canceled but in paid period - access through period end
if (user.subscriptionStatus === "canceled" &&
user.currentPeriodEnd &&
Date.now() < user.currentPeriodEnd) {
return true;
}
// 3. Past due with grace period
if (user.subscriptionStatus === "past_due" &&
user.currentPeriodEnd &&
Date.now() < user.currentPeriodEnd) {
return true;
}
// 4. Locked states - explicitly deny (before trial check)
const lockedStates = ["incomplete", "unpaid", "expired"];
if (lockedStates.includes(user.subscriptionStatus)) {
return false;
}
// 5. Trial active - fallback for non-subscribers
if (user.trialEndsAt && Date.now() < user.trialEndsAt) {
return true;
}
return false;
}
Key insight: Locked states block before trial check. This prevents edge cases where trial data could grant access.
Edge Cases & Handling
User Cancels During Trial
subscriptionStatus= "canceled"trialEndsAtalready cleared (was set to 0 when sub activated)- Access continues until
currentPeriodEnd(from Stripe trial_end) - No zombie trial risk
User Resubscribes After Cancel
- New checkout creates new subscription
- Fresh
trial_endcalculation (likely 0 - no trial remaining) - Billing starts immediately
User Never Had Trial (Direct Subscribe)
- No
trialEndsAtor_creationTimefor trial calc trial_endnot passed to Stripe- Billing starts immediately (correct)
Webhook Arrives Out of Order
- Use
eventTimestampcomparison - Reject events older than last processed
- Use
eventIdfor exact deduplication
Webhook Event Handling
Essential Events
| Event | Action |
|---|---|
checkout.session.completed |
Link customer, initial status |
customer.subscription.created |
Set status, period end |
customer.subscription.updated |
Update status, period end |
customer.subscription.deleted |
Set status to canceled/expired |
invoice.payment_succeeded |
Update period end |
invoice.payment_failed |
Set status to past_due |
Idempotency Pattern
// Check for duplicate event
if (user.lastStripeEventId === eventId) {
return { success: false, reason: "duplicate_event" };
}
// Check for stale event
if (user.lastStripeEventTimestamp &&
eventTimestamp < user.lastStripeEventTimestamp) {
return { success: false, reason: "stale_event" };
}
// Process and record
await db.patch(user._id, {
// ... updates
lastStripeEventId: eventId,
lastStripeEventTimestamp: eventTimestamp,
});
Testing Checklist
Trial Flow
- New user gets 14-day trial
- Trial countdown displays correctly
- Access denied after trial expires (hard cutoff)
Subscribe During Trial
- Remaining trial days passed to Stripe
- Stripe subscription shows trial_end
- No charge until trial_end
- trialEndsAt cleared in database
Cancel Flow
- Access continues until currentPeriodEnd
- No zombie trial access after period ends
- Resubscribe starts billing immediately
Webhook Handling
- Out-of-order events handled correctly
- Duplicate events rejected
- Fallback works if customer not linked
Common Pitfalls
1. Lazy Trial Calculation Gone Wrong
Problem: Calculating trial from _creationTime without clearing it.
Fix: Always clear trialEndsAt when subscription activates.
2. Checking Trial Before Locked States
Problem: User with status=expired might have valid trial dates.
Fix: Check locked states before trial in access logic.
3. Missing currentPeriodEnd
Problem: Some webhooks don't include period end.
Fix: Always set it from invoice.payment_succeeded as backup.
4. Not Using Stripe's trial_end
Problem: Custom trial logic that diverges from Stripe.
Fix: Let Stripe manage trial via trial_end parameter.
Related Skills
billing-security- Security patterns for payment integrationsstripe-health- Webhook health diagnosticsreconciliation-patterns- Syncing external service state
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?