Stripe
Synchronizing Stripe subscription state with a database
Keep a plan column in Prisma aligned with Stripe subscription status using Checkout metadata and subscription events.
Your app should not call stripe.subscriptions.retrieve on every dashboard render. It should read a column that a webhook keeps current. The Stripe Subscription Starter stores that column as User.plan, plus stripeCustomerId and stripeSubscriptionId. The Complete SaaS Starter Kit uses the same fields on PostgreSQL.
The mapping
function planFor(status: string) {
return status === "active" || status === "trialing" ? "pro" : "free";
}
Stripe’s subscription status is a string. The kit treats active and trialing as Pro. Every other status the webhook receives, including canceled, past_due, unpaid, and incomplete, becomes free. There is no past_due value in the database.
Two writes, two keys
checkout.session.completed knows your user id because Checkout was created with metadata.userId. That event sets:
planto"pro"stripeCustomerIdfromsession.customerstripeSubscriptionIdfromsession.subscription
Later events do not include userId. They include subscription.id. The handler finds the row with:
await prisma.user.updateMany({
where: { stripeSubscriptionId: subscription.id },
data: { plan: planFor(subscription.status) }
});
If the completed event never arrived, stripeSubscriptionId is still null and the update matches nothing. Order matters. Handle checkout.session.completed before you rely on subscription events, or also match on subscription.customer against stripeCustomerId. The kit matches only the subscription id.
What the column is for
Server components and route handlers call currentUser() and branch on plan. The AI SaaS Starter uses the same field to pick a monthly credit allowance: Pro reads PRO_MONTHLY_CREDITS (default 500), everyone else reads FREE_MONTHLY_CREDITS (default 20). A stale plan means a canceled customer keeps the higher cap. That is why the webhook is part of billing, not an optional log.
What is not stored
The kit does not copy invoice ids, period end, or the price id. You cannot render “renews on” from the database. If you need that date, add a column and set it from subscription.current_period_end in the same updated handler. Until you do, ask Stripe when you need it, or show nothing.
Want to skip the setup? The Stripe Subscription Starter already writes plan from Checkout and from subscription updates and deletions.