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.

2 min read

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:

  • plan to "pro"
  • stripeCustomerId from session.customer
  • stripeSubscriptionId from session.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.

Product updates for developers shipping with Designyff kits.

Purchased source may be used in personal, commercial, and client projects. Do not redistribute or resell the original source. Terms of Service

Designyff © 2026 | Starter kits for developers | Powered by  Tacko