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
| Variable | Purpose |
|---|---|
NUXT_PUBLIC_PAYMENT_PROVIDER | Set to stripe to select this provider. |
NUXT_STRIPE_API_KEY | Secret/restricted key (sk_... or rk_...); also gates registration. |
NUXT_STRIPE_WEBHOOK_SECRET | Signing secret (whsec_...) used to verify webhook signatures. |
NUXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY / _PRO_YEARLY / _LIFETIME | Public 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".
Checkout
createPayment maps the plan's productType to a Checkout mode:
productType | Mode | Result |
|---|---|---|
month / year | subscription | Recurring subscription |
one_time | payment | One-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-signatureheader and callsconstructStripeEvent(which usesNUXT_STRIPE_WEBHOOK_SECRET). A missing signature returns401; an invalid one returns400. Verified events are dispatched tohandleStripeEvent. - Handled events:
| Event | Effect |
|---|---|
checkout.session.completed / checkout.session.async_payment_succeeded | If paid, mark the payment row success; for payment mode, grant lifetime access. |
checkout.session.async_payment_failed | Mark the payment row failed. |
checkout.session.expired | Mark the payment row closed. |
customer.subscription.created / updated / deleted | Upsert the user's subscription row (status, period, trial, cancel timestamps). |
invoice.paid | Stamp lastPaidAt on the matching subscription. |
charge.refunded / refund.created | Mark 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/stripeThe 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/stripeand subscribe to the events listed above; copy its signing secret intoNUXT_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
activesubscriptionrow with the correct period. - A lifetime (one-time) purchase creates a
one_timerow with a far-futureperiodEnd. -
Manage billingopens the Stripe Billing Portal. - An invalid webhook signature returns
400.
