Stripe billing guide

Stripe subscription access checklist for SaaS apps

Use this checklist before launch if your app depends on Stripe subscriptions, entitlements, Supabase, or a custom database field to decide who gets paid access.

Stripe can correctly charge a customer while your SaaS still gives the wrong access. That is the dangerous part. The payment can succeed, the webhook can return 200 OK, and the dashboard can look clean while your database still says the user is free, canceled, trialing, or paid incorrectly.

This guide is for founders building AI-assisted SaaS apps with Lovable, Cursor, Bolt, Replit, v0, Supabase, Next.js, or similar stacks. It focuses on the Stripe events that usually decide whether a user should keep, lose, or change paid access: customer.subscription.deleted, entitlements.active_entitlement_summary.updated, invoice.paid, invoice.payment_failed, and customer.subscription.updated.

Launch rule: do not ship paid accounts until you can prove Stripe subscription state and your app access state match after checkout, failed payment, plan change, cancellation, duplicate webhook delivery, and delayed webhook delivery.

The real bug: Stripe state and app access drift apart

Most subscription bugs are not "Stripe is broken" bugs. They are mapping bugs. Stripe knows the customer, subscription, invoice, product, price, and entitlement. Your app knows the internal user, workspace, team, role, plan, feature flags, and database permissions. The risk lives between those systems.

When that bridge is weak, these failures happen:

  • A canceled customer keeps Pro access because customer.subscription.deleted does not update your database.
  • A paid customer stays locked out because invoice.paid updates the wrong user or no user.
  • A downgraded customer keeps premium features because entitlements are never re-synced.
  • A failed renewal leaves the app in an unclear state because invoice.payment_failed only logs and returns 200.
  • A duplicate webhook creates duplicate rows or overwrites a newer state with an older one.

What should customer.subscription.deleted do in a SaaS app?

Stripe's event type reference says customer.subscription.deleted occurs when a customer's subscription ends. In a SaaS app, this event should lead to a clear access decision.

Do not let this event only log a message. Decide the policy and encode it:

  • Remove paid access immediately when the subscription is canceled.
  • Or keep access until current_period_end if your cancellation policy allows paid time to continue.
  • Update the same backend source of truth used by your app to gate features.
  • Record the Stripe subscription ID, customer ID, previous status, new status, and event ID.
  • Notify the user or workspace owner when access will end.

The most common mistake is updating a cosmetic field such as subscription_status while the actual app still gates access from a different field like role, plan, or has_pro.

What is entitlements.active_entitlement_summary.updated?

entitlements.active_entitlement_summary.updated is a Stripe event for apps that use Stripe Entitlements to control feature access. If you use Stripe Entitlements, it is one of the most important events for product access. Stripe's subscription webhook docs state that this event is sent when a customer's active entitlements are updated, and that you can provision or de-provision product features when you receive it.

This is different from simply knowing whether the subscription is active. Entitlements answer the practical question: which features should this customer have right now?

Use entitlements when access is feature-based

For example, your app might have:

  • AI credits per month.
  • Team seats.
  • Export access.
  • Premium templates.
  • Private workspace access.
  • Higher rate limits.

When entitlements change, your backend should update the customer's feature access or mark it for reconciliation. The frontend should not guess access from a checkout success page or a cached plan name.

What should invoice.payment_failed do?

invoice.payment_failed means Stripe could not collect payment for an invoice. In a SaaS app, this event should not only be logged. It should move the account into a deliberate payment-risk state and trigger your chosen access policy.

Before launch, decide how failed payments affect the product:

  • Notify the customer or workspace owner that payment failed.
  • Start a grace period if your business allows temporary access.
  • Mark the subscription or workspace as past_due, payment_failed, or another clear internal state.
  • Restrict paid features when the grace period ends.
  • Make backend routes read from the same payment-risk state as the UI.

The dangerous version is a webhook handler that receives invoice.payment_failed, returns 200, and leaves the app access state unchanged forever.

What about customer.subscription.created, updated, and trial_will_end?

Stripe subscription apps should also understand the surrounding subscription lifecycle events. customer.subscription.created is usually where a new subscription first appears, customer.subscription.updated can reflect plan, status, trial, pause, or cancellation-at-period-end changes, and customer.subscription.trial_will_end is a warning that a trial is about to convert or end.

Do not treat these events as noise. Before launch, decide which events only notify users and which events change app access. For example, customer.subscription.trial_will_end may only send an email, while customer.subscription.updated may need to change plan limits, seats, feature flags, or renewal state.

Minimum Stripe events to handle before launch

A subscription SaaS can have a simple product, but the lifecycle is not simple. Before launch, make sure your webhook explicitly handles these events or has a documented reason not to:

  • checkout.session.completed - connect the initial checkout to your internal user or workspace.
  • customer.subscription.created - record the new Stripe subscription and map it to the correct internal user, team, or workspace.
  • invoice.paid - provision or continue access after successful invoice payment when the subscription is active.
  • invoice.payment_failed - mark payment risk, start a grace period, notify the customer, or restrict access based on policy.
  • customer.subscription.updated - handle status changes, plan changes, trials, pauses, discounts, and renewal state.
  • customer.subscription.trial_will_end - notify the customer and prepare the app for trial conversion or access changes.
  • customer.subscription.deleted - revoke or schedule removal of subscription access.
  • entitlements.active_entitlement_summary.updated - provision or de-provision feature-level access if you use Stripe Entitlements.

Do not trust a 200 OK delivery by itself

Stripe recommends returning a successful 2xx response quickly before complex logic that might time out. That is correct for webhook delivery reliability, but it can confuse founders. A successful webhook delivery means Stripe received a successful HTTP response. It does not prove your app updated the right database row.

For launch safety, trace the event all the way through:

  1. Stripe event created.
  2. Webhook endpoint received the event.
  3. Signature verified with the raw request body.
  4. Event ID checked for duplicate processing.
  5. Stripe customer or subscription mapped to internal user or workspace.
  6. Subscription or entitlement state written to the correct table.
  7. Frontend and API routes read from that same access source of truth.

If you cannot name the exact table and field that gates paid access, the billing system is not ready for launch.

Database checks for Supabase and AI-built apps

In many AI-built SaaS apps, Stripe writes are added after the UI is already working. That creates a common split: the UI has a plan concept, Supabase has a profiles table, Stripe has subscriptions, and server routes check something else.

Check the access source of truth

  • Where is paid access stored: profiles, accounts, workspaces, subscriptions, or entitlements?
  • Is access tied to a user, team, workspace, or organization?
  • Can one Stripe customer map to multiple internal users?
  • Can one workspace have multiple seats?
  • What happens when the owner cancels but members are still logged in?

Check direct API access

Do not only test the UI. Test server routes directly. A canceled user should not be able to call paid endpoints directly if the UI button disappears. The backend must enforce paid access too.

Duplicate events and delayed events

Stripe can retry events, and Stripe does not guarantee event delivery order. Your handler should be safe when an event arrives twice, arrives late, or arrives in a surprising order.

At minimum:

  • Store processed Stripe event IDs.
  • Skip or safely reprocess events already handled.
  • Fetch the latest subscription, invoice, customer, or entitlement state from Stripe when final state matters.
  • Do not let an older event overwrite a newer subscription state.
  • Log enough data to debug user ID, customer ID, subscription ID, event ID, old state, and new state.

Test cases before you accept real payments

Run these tests in Stripe test mode and your staging or production-like environment:

  1. New checkout: a new user pays and gets the correct access.
  2. Wrong user protection: the same checkout cannot grant access to another account.
  3. Renewal success: invoice.paid keeps or refreshes access.
  4. Failed payment: invoice.payment_failed changes state according to your policy.
  5. Cancellation: customer.subscription.deleted removes or schedules removal of access.
  6. Plan upgrade: paid features increase correctly.
  7. Plan downgrade: premium features are removed correctly.
  8. Entitlement update: feature access changes when entitlements.active_entitlement_summary.updated fires.
  9. Duplicate delivery: sending the same event twice does not create duplicate access changes.
  10. Out-of-order delivery: a later state cannot be overwritten by an older event.

Launch verdict

Block ship if users can pay but your app cannot prove access changes for cancellation, failed payment, plan change, duplicate event, and entitlement update.

Ship with fixes if checkout works and access is mostly correct, but there is no reconciliation job, no alerting, or no duplicate event protection.

Clear to ship when Stripe, your database, and your API access checks agree across the full subscription lifecycle.

Useful references

Want this checked before users pay?

I review Stripe subscription flows, entitlement updates, Supabase access state, exposed keys, and launch-blocking billing drift in AI-built SaaS apps.

Book $299 Launch Audit View sample report