Skip to content

Stripe payments

This project integrates Stripe for hosted Checkout (subscriptions and one-time payments) and the Billing Customer Portal. Server logic lives in src/lib/stripe.ts; checkout and portal are invoked from src/actions/payment-actions.ts. Stripe notifies your app via POST webhooks on /api/stripe (src/app/api/stripe/route.ts), which updates User fields in Prisma (priceId, hasAccess, customerId, subscribedAt, cancelAtPeriodEnd, etc.).

Billing UI is linked from the dashboard at appConfig.stripe.billingRoute (/dashboard/billing).

Stripe Dashboard setup

Complete these steps in Stripe Dashboard:

  1. Account — Create or use a Stripe account; finish activation so you can accept real payments when ready.
  2. Public / business details — Add your site URL and company details where Stripe requires them.
  3. Branding — Logo and colors (shown on Checkout and portal where applicable).
  4. Customer emails — Enable receipts and related customer emails if you want Stripe to send payment emails in addition to app emails.
  5. Customer portalSettings → Billing → Customer Portal: configure which features customers can manage (payment methods, cancel subscription, etc.). The app calls billingPortal.sessions.create for users who already have a Stripe customerId.
  6. Fraud / 3DS — Under Rules (fraud prevention), keep 3D Secure rules aligned with your risk tolerance (e.g. require 3DS when required/supported; consider blocking on failed CVC where appropriate).
  7. Test mode — Use Test mode while developing; switch to Live mode for production keys and webhooks.

TIP

After successful checkout, this app can send an order confirmation email via Resend when a matching plan exists in appConfig.stripe.plans (src/app/api/stripe/route.ts). Ensure Resend is configured if you rely on that behavior.

Products and prices (plans)

  1. In Stripe → Product catalog, create products and prices for each plan (recurring subscription or one-time payment).
  2. Copy each Price ID (e.g. price_xxxxxxxxxxxxx).
  3. Add them to src/config.ts inside appConfig.stripe.plans: for each plan set priceId, title, price (display/ email), and mode as either "subscription" or "payment".

The server action createCheckoutSessionAction resolves the plan only from this config; unknown priceId values are rejected with “Plan not found”.

ts
// Example shape (use your own Price IDs)
plans: [
  { price: 99, title: "Starter", mode: "subscription", priceId: "price_..." },
  { price: 149, title: "Pro", mode: "subscription", priceId: "price_..." },
  { price: 499, title: "Lifetime", mode: "payment", priceId: "price_..." },
];

Marketing Pricing UI (src/components/pricing/index.tsx) is wired to the first three entries in appConfig.stripe.plans — keep that in mind if you change the number or order of plans.

Trial length for in-app copy / utilities uses appConfig.stripe.trailPeriod (milliseconds; example in repo: 7 days). Stripe-side trial behavior should match how you configure prices in the Dashboard if you use Stripe trials.

Environment variables

Defined in src/config.ts:

VariableUsed for
STRIPE_SECRET_KEYServer-side Stripe client (src/lib/stripe.ts); required — app throws if missing.
STRIPE_WEBHOOK_SECRETWebhook signature verification (src/app/api/stripe/route.ts).
STRIPE_PUBLIC_KEYPresent on appConfig.stripe.publicKey; hosted Checkout does not require it in current server-only flow, but keep it set if you add Stripe.js / Elements later.

Checkout success/cancel URLs and portal return URL use appConfig.domainUrl (from AUTH_URL / NEXT_PUBLIC_APP_URL / VERCEL_URL / defaults in src/config.ts). Set these correctly in dev and prod so redirects land on your app.

Webhook events (this project)

Register a webhook endpoint that sends POST requests to:

text
https://YOUR_DOMAIN/api/stripe

Local development (Stripe CLI):

bash
stripe listen --forward-to localhost:3000/api/stripe

Use the CLI’s signing secret as STRIPE_WEBHOOK_SECRET for local .env.

Events handled

EventPurpose in this codebase
checkout.session.completedResolves user via client_reference_id (preferred) or checkout email; sets priceId, hasAccess: true, customerId, subscribedAt; sends order confirmation email if plan is in config.
customer.subscription.updatedUpdates priceId, hasAccess (active/trialing), cancelAtPeriodEnd when cancellation is scheduled or cleared.
customer.subscription.deletedRevokes access: hasAccess: false, clears priceId and cancelAtPeriodEnd.

Other event types are accepted but ignored (handler returns success).

WARNING

If neither client_reference_id nor customer_details.email matches a user, the handler still returns 200 and skips DB updates — ensure Checkout always sends client_reference_id (this app passes userId from createCheckoutSessionAction).

Checkout and portal flows

ActionEntryBehavior
CheckoutcreateCheckoutSessionActionAuthenticated users only. Builds a session with line_items, mode, client_reference_id, and allow_promotion_codes: true, then redirects to Stripe-hosted Checkout. Success and cancel URLs return to ${domainUrl}/dashboard with session_id and status (success or failed).
Customer portalcreateCustomerPortalActionRequires a stored customerId on the user. return_url is ${domainUrl}/dashboard.

Stripe API version pinned in code: 2026-03-25.dahlia (src/lib/stripe.ts) — upgrade the SDK and comment when changing versions.

Production checklist

  • [ ] Live mode keys: STRIPE_SECRET_KEY (live sk_live_…).
  • [ ] Webhook: Dashboard → Developers → Webhooks → add endpoint https://yourdomain.com/api/stripe with the events above (at minimum those three).
  • [ ] STRIPE_WEBHOOK_SECRET: use the signing secret from the live webhook endpoint (not the test CLI secret).
  • [ ] Price IDs in appConfig.stripe.plans match live Stripe prices.
  • [ ] appConfig.domainUrl matches your production URL (redirects and emails).
  • [ ] Customer portal activated and configured in Stripe.
  • [ ] Resend / email env vars if you rely on order confirmation emails.

File map

RolePath
Plan config, billing route, env mappingsrc/config.ts
Stripe client, Checkout, Portalsrc/lib/stripe.ts
Server actions (checkout + portal)src/actions/payment-actions.ts
Webhook handlersrc/app/api/stripe/route.ts
Typessrc/types/stripe.ts
Pricing section (example)src/components/pricing/index.tsx
Billing dashboardunder /dashboard/billing (see appConfig.stripe.billingRoute)

Built with Nexus Orbit