DEV Community

Visoar Zhang
Visoar Zhang

Posted on Originally published at starter.ullrai.com Fully Autonomous

Stripe billing in Next.js: checkout, idempotent webhooks, and release checks

I help maintain UllrAI SaaS Starter, an MIT Next.js project. This article was prepared with AI assistance and checked against the linked implementation.

Adding a checkout button is the shortest part of a billing integration. Reliability depends on what happens before the redirect, after the provider callback, during retries, and when test and live catalogs diverge. Here are the boundaries used in our starter, with source paths you can inspect.

Keep product intent independent from the provider

The application contract lives in src/lib/billing/provider.ts. Callers provide a tier ID, payment mode, optional billing cycle, authenticated owner, and return URLs. They receive a validated checkout URL rather than a raw provider response.

The Stripe adapter maps that intent to a Price ID in src/lib/billing/stripe/prices.ts. Subscription and one-time purchase flows share the contract while retaining different entitlement behavior. This keeps business code separate without pretending every provider has identical concepts.

Validate checkout before leaving the application

POST /api/billing/checkout is an authenticated boundary. It validates the feature flag, session, supported tier/payment mode/billing cycle, the active environment's Price ID, and conflicting entitlements. Repeated browser attempts reuse a request ID to avoid accidental duplicate checkout sessions.

The client allowlists the returned redirect host before navigation. A successful HTTP response containing an unexpected URL becomes a controlled error.

Separate test and live catalogs

STRIPE_ENVIRONMENT selects test_mode or live_mode. Price IDs are stored separately. The sync command updates only the active namespace:

pnpm stripe:sync-products
Enter fullscreen mode Exit fullscreen mode

Review and commit the resulting catalog configuration. A missing live Price ID must not fall back to the test catalog or cause an application request to create Products or Prices.

Treat webhooks as durable billing evidence

The browser returning from checkout is feedback, not durable billing evidence. The webhook verifies the Stripe signature, records the provider event ID, and processes the event idempotently. Relevant transitions include checkout completion, subscription renewal or cancellation, payment failure, refunds, and disputes.

Ownership metadata is allowlisted and checked against the checkout record. Product and payment-mode metadata must match the configured tier before an entitlement is written. Retries distinguish an event that was already processed from processing that failed before completion, so replay does not apply the same entitlement twice.

Inspect the Stripe implementation and webhook route when adapting this flow.

Verify the return path without trusting it

The payment status page polls the application API using the checkout reference. The server retrieves the checkout and verifies ownership. The client displays success, pending, failed, or cancelled from this controlled result.

Analytics records payment_start before redirect and payment_success only when the controlled status reaches success. Webhooks and database rows remain the accounting source; analytics events describe the journey.

Release checks

  1. Configure the live Stripe key and endpoint signing secret.
  2. Sync and review the live Price IDs.
  3. Register /api/billing/webhooks/stripe as the production webhook endpoint.
  4. Exercise subscription and one-time checkouts with an authenticated test account.
  5. Replay a webhook and verify that database state changes only once.
  6. Test cancellation, refund, and failed-payment paths.
  7. Run pnpm lint, pnpm type-check, pnpm test, and pnpm build.

Keep secrets and customer payment data out of verification logs.

Add a new tier through the typed product catalog and its tests. A new provider should implement the existing contract while preserving ownership, validation, and idempotency.

The original billing guide, architecture walkthrough, and developer guide provide the surrounding setup and deployment details.

Top comments (0)