Subscription Models
Subscriptions are recurring payments—like a gym membership for your app. Users pay monthly or yearly, you get predictable revenue. Stripe handles the complexity.
How Subscriptions Work
Think of it like Netflix: user signs up → picks a plan → enters payment → gets billed automatically every month. Stripe manages the entire lifecycle:
Checkout
User picks plan & pays
Billing Cycle
Auto-charged monthly/yearly
Recurring Revenue
Predictable income
Stripe Subscription Concepts
Products
What you're selling. Example: "SaucyTech Builder" plan. Created once in Stripe Dashboard.
Prices
How much and how often. One product can have multiple prices: $19/month and $149/year. Each price has a unique price_id.
Subscriptions
The actual billing relationship. Links a Customer to a Price. Has status: active, canceled, past_due, etc.
Customers
Stripe's record of a paying user. Store the stripe_customer_id in your database linked to your user.
The Subscription Flow
User clicks "Subscribe"
Your app creates a Stripe Checkout Session with the price_id.
Stripe Checkout handles payment
User enters card details on Stripe's hosted page. You never touch card numbers.
Webhook confirms success
Stripe sends checkout.session.completed. You update the user's subscription status.
Access granted
Check subscription_status === "active" before showing premium features.
Auto-renewal
Stripe charges automatically each cycle. Webhooks notify you of success or failure.
Creating a Checkout Session
// app/api/stripe/checkout/route.ts
import
Stripe
from
"stripe"
;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const { priceId, userId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{
price: priceId,
quantity: 1,
}],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: { userId }, // Link to your user!
});
return Response.json({ url: session.url });
}
The metadata trick
Pass your userId in metadata. When the webhook fires, you'll know which user to update.
Upgrades & Downgrades
Upgrade
Monthly → Yearly. Stripe prorates: user pays the difference immediately.
Downgrade
Yearly → Monthly. User keeps access until period ends, then new price kicks in.
// Upgrade a subscription to a new price
await stripe.subscriptions.update(subscriptionId, {
items: [{
id: existingItemId,
price: newPriceId,
}],
proration_behavior: "create_prorations",
});
Trial Periods
Let users try before they buy. Stripe won't charge until the trial ends.
// Add a 7-day trial to checkout
const session = await stripe.checkout.sessions.create({
mode: "subscription",
subscription_data: {
trial_period_days: 7,
},
// ... rest of config
});
Trial Status
During trial, subscription status is trialing. Check for both active and trialing to grant access.
Common Pitfalls
Checking payment at checkout only
Subscriptions can fail, cancel, or expire. Checking only at signup misses changes.
Fix: Listen to webhooks for subscription changes. Always check current status before granting access.
Not handling failed payments
Cards expire, get declined, or hit limits. Subscription goes past_due.
Fix: Listen for invoice.payment_failed. Email the user. Stripe auto-retries, but you should notify them.
Proration surprises
User upgrades mid-cycle and gets charged unexpectedly (or you lose money on downgrades).
Fix: Understand proration. Show users what they'll pay. Use proration_behavior to control timing.
Using test keys in production
sk_test_ keys don't process real payments.
Fix: Use sk_live_ in production env vars. Double-check before launch!
Handle subscription events
Webhooks tell you when subscriptions change. Learn to handle them reliably.
Webhooks Guide →