STRIPE STARTER · PROJECT STRUCTURE
Project structure
A tour of the folders that matter, the files you'll edit, and the ones you can safely ignore.
We tried to keep the layout boring on purpose. If you've used Next.js App Router, nothing here will surprise you. The two intentional bits are: payments.config.ts lives at the project root (not under src), and the webhook route is the ONLY thing under /api. Everything else is a Server Action or a Server Component.
Top-level
payments.config.ts // mode flag — the brain of the whole app
.env.example // copy to .env.local
src/
├── app/
│ ├── (marketing)/ // public marketing pages (incl. /pricing)
│ ├── checkout/ // success + cancel pages
│ ├── api/webhooks/ // THE ONLY API ROUTE — Stripe webhooks
│ ├── page.tsx // landing
│ ├── error.tsx // global error boundary
│ └── not-found.tsx // custom 404
│
├── actions/
│ └── checkout.ts // createCheckoutSession, createPortalSession
│
├── lib/
│ ├── env.ts // T3 Env + Zod — every env var declared once
│ ├── stripe.ts // server SDK + helpers (getCustomerId, getResourceId)
│ ├── stripe-client.ts // browser SDK (lazy, only for Elements/Embedded)
│ ├── payments.ts // getPaymentsConfig + isEventRelevant
│ ├── validation.ts // every Zod schema, centralized
│ ├── rate-limit.ts // Upstash limiters (graceful when missing)
│ ├── utils.ts // cn() — clsx + tailwind-merge
│ └── stripe-events/ // ONE FILE PER EVENT
│ ├── on-checkout-session-completed.ts
│ ├── on-invoice-paid.ts
│ ├── on-subscription-updated.ts
│ └── on-subscription-deleted.ts
│
├── config/
│ └── pricing.ts // plans, prices, features, price IDs, helpers
│
├── components/
│ ├── checkout/ // CheckoutButton, CustomerPortalButton
│ ├── pricing/ // PricingTable, PricingCard, PricingToggle
│ └── ui/ // shadcn/ui — DO NOT edit, extend by wrapping
│
├── hooks/
│ ├── use-payments-config.ts // client-side mode + derived flags
│ ├── use-pricing.ts // monthly/yearly toggle state
│ └── use-mobile.ts
│
├── types/
│ ├── payments.ts // PaymentMode + PaymentsConfig
│ └── stripe.ts // CheckoutResult / PortalResult discriminated unions
│
└── test/setup.ts // jest-dom matchers — loaded by vitestThe files you'll actually touch
- payments.config.ts — flip mode, toggle the portal, change redirect paths. 80% of customization happens here.
- src/config/pricing.ts — add/remove plans, set price IDs, change features. The Server Action allowlists against this file.
- src/lib/stripe-events/*.ts — implement what happens when a customer pays. These start as stubs.
- src/components/pricing/*.tsx — restyle the pricing cards, change CTA text, customize layout.
- src/app/checkout/success/page.tsx — customize the post-payment thank-you experience.
The files you almost never touch
- src/lib/stripe.ts — server SDK instance. Edit only when adding a helper.
- src/actions/checkout.ts — the Server Actions. Edit only to add a new action or change rate limits.
- src/app/api/webhooks/stripe/route.ts — verifies signatures, routes to handlers. The switch is the only line you might touch (to add a new event).
- src/lib/validation.ts — Zod schemas. Add a new schema here, don't validate inline.
- src/components/ui/* — generated by shadcn/ui. Don't edit, extend by wrapping.
Why the webhook is the only API route
Stripe needs to POST a raw, unmodified request body to verify the signature. Next.js API routes give us that control. Everything else — checkout, portal — uses Server Actions, which are typed, RPC-style, and skip the round-trip of writing/maintaining an API contract. If you find yourself reaching for /api/something-else, ask whether a Server Action would do.