Stripe

Stripe Checkout, subscriptions, lifetime, and the billing portal for the Nuxt template.

Stripe implements the PaymentProvider interface in server/payment/providers/stripe-provider.ts and is registered by the factory when NUXT_STRIPE_API_KEY is set. It uses Stripe Checkout for recurring plans and one-time (lifetime) purchases, syncs subscriptions from webhooks, and exposes the Billing Portal.

Configuration

VariablePurpose
NUXT_PUBLIC_PAYMENT_PROVIDERSet to stripe to select this provider.
NUXT_STRIPE_API_KEYSecret/restricted key (sk_... or rk_...); also gates registration.
NUXT_STRIPE_WEBHOOK_SECRETSigning secret (whsec_...) used to verify webhook signatures.
NUXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY / _PRO_YEARLY / _LIFETIMEPublic Price IDs (price_...) for the three plans.

Stripe values are Price IDs (price_...), not Product IDs. The client is pinned to apiVersion: "2026-05-27.dahlia".

See Environment Variables.

Checkout

createPayment maps the plan's productType to a Checkout mode:

productTypeModeResult
month / yearsubscriptionRecurring subscription
one_timepaymentOne-off lifetime purchase

It attaches metadata (orderNo, productId, productType, userId) to the session — and, for subscriptions, to subscription_data. If the user already has a Stripe customerId, the session reuses it; otherwise Stripe creates a customer from customer_email. Returns { orderNo, thirdOrderNo, checkoutUrl }.

Webhook

  • Path: POST /api/payment/notify/stripe (notify/stripe.post.ts).
  • Verification: the route reads the raw body and stripe-signature header and calls constructStripeEvent (which uses NUXT_STRIPE_WEBHOOK_SECRET). A missing signature returns 401; an invalid one returns 400. Verified events are dispatched to handleStripeEvent.
  • Handled events:
EventEffect
checkout.session.completed / checkout.session.async_payment_succeededIf paid, mark the payment row success; for payment mode, grant lifetime access.
checkout.session.async_payment_failedMark the payment row failed.
checkout.session.expiredMark the payment row closed.
customer.subscription.created / updated / deletedUpsert the user's subscription row (status, period, trial, cancel timestamps).
invoice.paidStamp lastPaidAt on the matching subscription.
charge.refunded / refund.createdMark the matching payment row refunded.

Subscription sync

syncStripeSubscription maps the Stripe status to the local enum (active / trialing / canceled / expired / unpaid), derives month vs year from the price interval, and writes the period window.

Each user has a single entitlement row. A guard prevents a late event from an old recurring subscription from overwriting a one_time (lifetime) purchase. Keep this guard if you customize the sync.

Lifetime purchases

For payment-mode checkouts, syncLifetimeAccess writes a one_time subscription row with status: active and periodEnd far in the future, so the user has permanent access without a Stripe subscription object.

Billing portal

createStripePortal opens a Stripe Billing Portal session. POST /api/payment/portal (portal.post.ts) reads the user's subscription.customerId and returns the portal URL — but only when the stored subscription.provider is stripe; otherwise it returns 400. The ManageBillingButton.vue component links to it.

Local testing

Use the Stripe CLI to forward events to your dev server (nuxt dev runs on port 3000):

stripe listen --forward-to localhost:3000/api/payment/notify/stripe

The CLI prints a whsec_... secret — set it as NUXT_STRIPE_WEBHOOK_SECRET. Then complete a checkout with a test card (e.g. 4242 4242 4242 4242) and confirm the payment row flips to success.

Production notes

  • Use live keys and live Price IDs.
  • Register a webhook endpoint at /api/payment/notify/stripe and subscribe to the events listed above; copy its signing secret into NUXT_STRIPE_WEBHOOK_SECRET.
  • Add idempotency — Stripe retries deliveries.
  • Prefer a restricted key (rk_...) scoped to Checkout, Customers, Subscriptions, and Billing Portal.

Verify

  • With NUXT_PUBLIC_PAYMENT_PROVIDER=stripe, the pricing CTA opens Stripe Checkout.
  • A subscription purchase creates an active subscription row with the correct period.
  • A lifetime (one-time) purchase creates a one_time row with a far-future periodEnd.
  • Manage billing opens the Stripe Billing Portal.
  • An invalid webhook signature returns 400.

On this page