Most solo developers and indie hackers are leaving money on the table with their Stripe setup. The default integration handles payments — but the optimized setup maximizes revenue through trials, annual plans, usage-based billing, and smart recovery flows.
Free Trial Architecture
// Create a subscription with a 14-day trial
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
trial_period_days: 14,
payment_settings: {
save_default_payment_method: 'on_subscription',
},
trial_settings: {
end_behavior: { missing_payment_method: 'pause' }, // pause if no card added
},
})
// In your webhook handler
case 'customer.subscription.trial_will_end':
// Fires 3 days before trial ends
await sendTrialEndingEmail(subscription.customer, subscription.trial_end)
break
Annual Plans with Savings
Annual plans improve cash flow and reduce churn. Display the monthly equivalent with savings badge:
const PLANS = {
pro_monthly: { priceId: 'price_...', amount: 2900, interval: 'month' },
pro_annual: { priceId: 'price_...', amount: 24900, interval: 'year' },
}
// Display: $99/year = $8.25/mo — save 17%
function getMonthlyCost(plan) {
if (plan.interval === 'year') return plan.amount / 12
return plan.amount
}
function getSavings(monthly, annual) {
const annualMonthly = annual.amount / 12
return Math.round((1 - annualMonthly / monthly.amount) * 100)
}
Usage-Based Billing
Charge per API call, per seat, or per token:
// Create a metered price in Stripe
// aggregate_usage: 'sum' | 'max' | 'last_during_period'
// Report usage at the end of each billing period
async function reportUsage(subscriptionItemId: string, quantity: number) {
await stripe.subscriptionItems.createUsageRecord(
subscriptionItemId,
{
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment', // or 'set'
}
)
}
// Track in your own DB, report to Stripe daily or on each API call
await redis.incr(`usage:${userId}:${month}`)
Smart Dunning (Failed Payment Recovery)
Configure Stripe's Smart Retries in the dashboard. Also send proactive emails:
case 'invoice.payment_failed':
const invoice = event.data.object
const attemptCount = invoice.attempt_count
if (attemptCount === 1) {
// First failure -- send polite reminder
await sendPaymentFailedEmail(invoice.customer_email, {
updateUrl: stripe.billingPortal.sessions.create({ customer: invoice.customer }),
nextAttempt: invoice.next_payment_attempt,
})
} else if (attemptCount >= 3) {
// Final warning before cancellation
await sendFinalWarningEmail(invoice.customer_email)
}
break
case 'customer.subscription.deleted':
// Subscription cancelled after failed payments
await downgradeUser(event.data.object.customer)
await sendCancellationEmail(event.data.object.customer)
break
Customer Portal
Let customers manage their own subscription — no custom UI needed:
// app/api/billing/portal/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({ where: { id: session.user.id } })
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing`,
})
return Response.json({ url: portalSession.url })
}
The portal handles: upgrade/downgrade, cancel, payment method update, invoice history. Zero custom UI.
Proration on Plan Changes
// Upgrade mid-cycle -- Stripe handles proration automatically
await stripe.subscriptions.update(subscriptionId, {
items: [{ id: subscriptionItemId, price: newPriceId }],
proration_behavior: 'create_prorations', // default
billing_cycle_anchor: 'unchanged', // keep existing renewal date
})
The AI SaaS Starter at whoffagents.com ships with Stripe fully pre-wired: checkout, webhooks, customer portal, trial support, and subscription lifecycle handlers. $99 one-time.
Top comments (0)