June 3, 2026
10 min read
Subscriptions with Stripe: Data Model, Events, and Edge Cases in SaaS Projects
How to model the complete subscription lifecycle with Stripe Billing: from trial through cancellation and payment retries.
Leer en espanolThe Stripe Billing data model
Stripe Billing works with four main objects: Customer, Product, Price, and Subscription. The Customer represents the user or company paying. The Product describes what is being sold. The Price defines the amount, currency, and billing interval. The Subscription connects a Customer with one or more Prices and manages the billing lifecycle.
A common mistake is syncing only the active state of the subscription and assuming everything else is irrelevant. In practice, the system needs to react to state transitions, failed billing attempts, grace periods, and plan changes. All of that arrives through webhooks, not polling.
The events you cannot ignore
Stripe emits dozens of event types, but for a SaaS with subscriptions there are five that are critical: `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_succeeded`, and `invoice.payment_failed`. Every other event can be ignored initially; these five cannot.
The `customer.subscription.updated` event is especially important because it covers plan changes, scheduled cancellations, reactivations, and trial period updates. Its payload includes the previous and new state, which allows differential logic without querying the Stripe API.
Handling a failed payment: update status and notify the user.
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
const subscriptionId = invoice.subscription as string;
await db.subscriptions.update({
where: { stripeSubscriptionId: subscriptionId },
data: {
status: 'past_due',
lastPaymentError: invoice.last_finalization_error?.message ?? null,
},
});
await notifications.send({
type: 'payment_failed',
userId: invoice.customer as string,
retryAt: invoice.next_payment_attempt
? new Date(invoice.next_payment_attempt * 1000)
: null,
});
break;
} Edge cases in the subscription lifecycle
Trial ending is one of the most frequent cases: Stripe sends `customer.subscription.trial_will_end` three days before the trial period ends. That event is the right moment to remind the user to add a payment method before the first real billing attempt.
Plan upgrades and downgrades have their own proration logic. By default, Stripe generates an immediate credit or charge when switching a Price. If the intended behavior is different — for example, switching plans at the next cycle without prorating — you must configure `proration_behavior` explicitly when creating or updating the subscription.
- Listen to `trial_will_end` to prompt the user for a payment method in advance.
- Handle `subscription.updated` with differential logic on the `status` field.
- Configure `cancel_at_period_end` to allow end-of-cycle cancellations.
- Test the dunning flow using Stripe designated test cards.
Syncing Stripe state with your database
Your product database should be the source of truth for access and features. Stripe is the source of truth for payment state. Those two things must stay in sync, but they are not the same: a user may retain active access even while Stripe is in a grace period, depending on business policy.
The cleanest pattern is to store `stripeSubscriptionId`, `stripeCustomerId`, Stripe `status`, and `currentPeriodEnd` in the product subscriptions table. On any relevant webhook, update those fields and recalculate access. Never query the Stripe API at request time to check whether a user has access.
More articles
Back to articlesPayment Platforms in Mexico: Stripe, Conekta, Mercado Pago, and OpenPay
A technical and commercial comparison of the four most-used payment platforms for digital projects in Mexico.
June 3, 2026
7 min read
Shipping Platforms in Mexico: Skydropx, EnviosPerros, Pakke, and Enviame
How to choose between the main shipping aggregators for ecommerce in Mexico based on your volume, operations, and technical needs.
June 3, 2026
7 min read
Headless CMS in 2026: PayloadCMS, Strapi, Sanity, and Directus
Which headless CMS to choose based on project type, the level of technical control you need, and how you plan to model your content.
June 3, 2026
8 min read