Appearance
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:
- Account — Create or use a Stripe account; finish activation so you can accept real payments when ready.
- Public / business details — Add your site URL and company details where Stripe requires them.
- Branding — Logo and colors (shown on Checkout and portal where applicable).
- Customer emails — Enable receipts and related customer emails if you want Stripe to send payment emails in addition to app emails.
- Customer portal — Settings → Billing → Customer Portal: configure which features customers can manage (payment methods, cancel subscription, etc.). The app calls
billingPortal.sessions.createfor users who already have a StripecustomerId. - 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).
- 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)
- In Stripe → Product catalog, create products and prices for each plan (recurring subscription or one-time payment).
- Copy each Price ID (e.g.
price_xxxxxxxxxxxxx). - Add them to
src/config.tsinsideappConfig.stripe.plans: for each plan setpriceId,title,price(display/ email), andmodeas 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:
| Variable | Used for |
|---|---|
STRIPE_SECRET_KEY | Server-side Stripe client (src/lib/stripe.ts); required — app throws if missing. |
STRIPE_WEBHOOK_SECRET | Webhook signature verification (src/app/api/stripe/route.ts). |
STRIPE_PUBLIC_KEY | Present 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/stripeLocal development (Stripe CLI):
bash
stripe listen --forward-to localhost:3000/api/stripeUse the CLI’s signing secret as STRIPE_WEBHOOK_SECRET for local .env.
Events handled
| Event | Purpose in this codebase |
|---|---|
checkout.session.completed | Resolves 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.updated | Updates priceId, hasAccess (active/trialing), cancelAtPeriodEnd when cancellation is scheduled or cleared. |
customer.subscription.deleted | Revokes 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
| Action | Entry | Behavior |
|---|---|---|
| Checkout | createCheckoutSessionAction | Authenticated 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 portal | createCustomerPortalAction | Requires 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(livesk_live_…). - [ ] Webhook: Dashboard → Developers → Webhooks → add endpoint
https://yourdomain.com/api/stripewith 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.plansmatch live Stripe prices. - [ ]
appConfig.domainUrlmatches 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
| Role | Path |
|---|---|
| Plan config, billing route, env mapping | src/config.ts |
| Stripe client, Checkout, Portal | src/lib/stripe.ts |
| Server actions (checkout + portal) | src/actions/payment-actions.ts |
| Webhook handler | src/app/api/stripe/route.ts |
| Types | src/types/stripe.ts |
| Pricing section (example) | src/components/pricing/index.tsx |
| Billing dashboard | under /dashboard/billing (see appConfig.stripe.billingRoute) |