Map Lemon Squeezy events into a local subscription projection after you verify the signature. Treat subscription_updated as the provider’s current-state signal, keep a receipt for every delivery, and make your own entitlement rule explicit. A successful checkout page can refresh the experience; it should not be the authority that grants paid access.
This is the practical answer for a Next.js developer who has added Lemon Squeezy but has not yet written down what each lifecycle event changes. Lemon Squeezy documents subscription_created, subscription_payment_success, and subscription_updated as a useful minimum for subscriptions. Its lifecycle documentation also makes clear that cancellation, expiry, pause, recovery, and refunds are different facts. Your application still has to decide how those facts affect access.
Build one event-to-state boundary
Keep the provider SDK and webhook vocabulary at the edge. The route should verify the raw request body and signature, extract a small normalized record, claim a durable receipt, and update the subscription and entitlement projection through a server-side repository. The rest of the application reads the projection; it does not infer access from a redirect, a browser-supplied plan, or the last event name it saw.
Frontend Accelerator’s current product repository already separates this kind of provider work. Its payment adapter exposes signature verification, event mapping, checkout mapping, subscription mapping, and refund mapping. The current Lemon Squeezy adapter maps order_created, order_refunded, subscription_updated, and subscription_expired into the shared payment event vocabulary. That is a useful seam to extend, not proof that every Lemon Squeezy lifecycle policy is implemented for your product.
Recommendation: let verified provider data describe billing state, then make one deliberate application decision about access.
Original asset: the event-to-state mapping
Use this mapping as a starting specification. It is an application policy, not a claim that Lemon Squeezy makes the access decision for you.
1. subscription_created → establish the subscription record
After signature verification, store the Lemon Squeezy subscription ID, customer ID, product and variant identifiers, status, billing dates, and the server-controlled account reference carried through checkout custom data. Do not grant a broad entitlement merely because an order exists. Confirm that the product or variant maps to a plan your application recognizes.
2. subscription_updated → reconcile the normalized subscription projection
This is the primary reconciliation event. Lemon Squeezy says it fires after lifecycle changes and can keep an application current even when more granular events are not subscribed. Compare the provider’s subscription update timestamp with the stored projection, reject clearly stale changes, then write the normalized status, dates, plan mapping, and access outcome together. Store the raw event identifier separately so the same delivery cannot apply twice.
3. subscription_cancelled → preserve access until the verified end date, when that is your policy
Lemon Squeezy documents cancellation as a grace period: the subscription is cancelled but can be resumed until the next billing date, when it expires. If your product offers access through the paid period, record the cancellation and ends_at; do not revoke immediately just because the cancellation event arrived. If your business policy is different, make that exception explicit and test it.
4. subscription_expired → end recurring entitlement
Expiry is a stronger access transition than cancellation. It can follow an unresumed cancellation or the end of dunning for a past-due subscription. Update the subscription projection and revoke or downgrade the recurring entitlement according to the plan rule. Keep the receipt and prior normalized state so a support investigation can explain why access changed.
5. Payment success, failure, and recovery → record billing evidence without inventing policy
subscription_payment_success is useful for billing history; a recovery is accompanied by a successful payment event. A failed renewal is not automatically the same as an expired subscription. Decide whether a grace period keeps access active, and write that decision beside the plan. The event itself should update a receipt or billing-history record; the next verified subscription state should reconcile access.
6. Pause, resume, refunds, and unmapped events → make each outcome visible
Paused and resumed subscriptions need a documented access rule. A refund must be scoped to the order or subscription and your product’s refund policy; it should not blindly disable every product owned by a customer. For an event your adapter does not map, preserve a minimal redacted receipt, mark it for review, and return an intentional response only after deciding the safe handling path. Silent defaults turn a provider addition into a hidden authorization change.
Use a receipt before changing state
Webhook delivery is a transport mechanism, not a transaction boundary for your application. Lemon Squeezy documents up to three retries after a non-200 response and recommends storing events locally so they can be processed safely. A durable receipt gives each delivery a claim state such as received, processing, applied, ignored, or failed-terminal. Key it by a provider event identifier when available, and retain the provider resource ID and update timestamp for freshness decisions.
Make the receipt and projection write atomic when your database supports it. The operation should claim the event, check whether the subscription update is older than the stored state, write the new normalized subscription record, and update the entitlement projection. Keep external calls, email, analytics, and long reconciliation work outside that transaction.
// Pseudocode — adapt this to your database transaction API.
await billingRepository.applyVerifiedEvent({
receipt: { provider: "lemon_squeezy", eventId, eventName, receivedAt },
subscription: { providerSubscriptionId, status, updatedAt, planId },
entitlement: decideAccess({ status, endsAt, planId }),
});
// The repository should ignore a claimed receipt and reject stale updates.
The example is deliberately not copied from Accelerator. Its current shared domain normalizes only a subset of Lemon Squeezy event names. Add the event model, normalized fields, database transaction, and tests that your policy requires rather than pretending an adapter switch handles the full subscription lifecycle.
Separate provider facts from product policy
A provider can tell you that a subscription is cancelled, paused, expired, or past due. It cannot decide whether a customer retains a feature during a grace period, which variants unlock which capabilities, how a one-time purchase differs from recurring access, or how a human resolves an ownership conflict. Put those choices in a small function with named inputs and tests.
- Provider fact: the signed subscription status, dates, identifiers, and event type.
- Mapping rule: which server-controlled plan and account the provider record belongs to.
- Entitlement policy: the exact access state a protected route may read.
- Operator path: what happens when the identities, price mapping, or event order conflict.
This separation is especially useful when an AI coding agent helps with billing work. Give it the adapter contract, fixture names, and expected final projections. A route that returns 200 is not enough acceptance evidence; you need one intentional state after a duplicate delivery, an older delivery, and a retryable storage failure. The related webhook replay guide shows that test shape with Stripe, and the same boundary applies to Lemon Squeezy.
Test the transitions that change access
Start with a compact fixture pack from Lemon Squeezy test mode. Lemon Squeezy supports simulated subscription events in test mode, which lets you test lifecycle paths without changing live data. Keep fixtures redacted and represent only the fields your mapper needs.
- Apply
subscription_createdfollowed bysubscription_updated; assert the intended active projection once. - Apply the same delivery twice; assert one receipt and no second entitlement write.
- Apply a newer update, then an older update for the same subscription; assert the projection stays current.
- Apply cancellation with a future
ends_at; assert the grace-period policy rather than an accidental immediate revocation. - Apply expiry after cancellation; assert the recurring entitlement reaches its documented end state.
- Force a storage failure between receipt claim and projection completion; assert a retry has a safe, visible path.
- Apply an unknown event and an unknown variant; assert neither silently grants nor revokes access.
These tests do not make billing maintenance-free. They make the next production disagreement inspectable: you can see the provider fact, the mapping rule that ran, the final projection, and the receipt that led there.
Where this fits in a Next.js SaaS
Keep the raw webhook boundary on the server and verify the signature before parsing or writing state. In Frontend Accelerator, payment-provider logic is intentionally behind an adapter and the shared payment interface includes signature verification and mapping methods. That gives a product-specific implementation an obvious first place to add Lemon Squeezy lifecycle coverage, while protected routes can depend on a smaller entitlement projection.
Explore the provider-adapter boundary before adding provider calls to pages or components. The foundation supplies the structure; you still own plan mapping, lifecycle policy, reconciliation, monitoring, and the tests that prove paid access changes for the right reason.



