Tech Stack
The frameworks, libraries, and services the boilerplate is built on, and why.
A self-hostable ecommerce starter on Next.js 16 with the App Router and React 19. It runs with no configuration at all against a bundled demo catalog; for real data the recommended path is PostgreSQL through Prisma — or SQLite, which is the same adapter over a file — with Supabase and Firebase as alternatives behind the same interface.
Every version on this page is the range in package.json, and tests/docs-versions.test.ts fails the build if the two disagree. That guard exists because this page had drifted a full major version behind the code before anyone noticed.
What is required, and what is not
Nothing on the default path needs an account. The store boots on the bundled fixture catalog; each heavier capability turns on only when you set its variables.
| Capability | Turns on when you set | Without it |
|---|---|---|
| Real catalog data (SQLite) | COMMERCE_BACKEND=sqlite + NEXT_PUBLIC_COMMERCE_BACKEND=sqlite + DATABASE_URL="file:./dev.db" | The bundled 18-product demo catalog serves the store |
| Real catalog data (Postgres) | the same two vars set to postgres + a postgresql:// DATABASE_URL | As above |
| Sign-in, saved carts, order history | NEXT_PUBLIC_FIREBASE_* (plus a service account for server writes), or AUTH_PROVIDER=authjs on a SQL backend | Browsing and the cart work; anything per-customer is unavailable |
| Card payments | STRIPE_SECRET_KEY (plus STRIPE_WEBHOOK_SECRET to record orders) | Demo checkout confirms locally; a configured store falls back to the quote flow |
| Transactional email | RESEND_API_KEY (plus RESEND_FROM_EMAIL) | Quotes submit normally, emails are skipped |
Data and identity are separate choices. Both are swappable, and independently: COMMERCE_BACKEND names who holds the data, AUTH_PROVIDER names who says which visitor is calling. The one pairing that is not free is Firestore-as-data, which needs Firebase Auth because the shipped rules gate on request.auth. See authentication.
Core stack
The load-bearing dependencies every install uses.
| Package | Version | Role |
|---|---|---|
next | ^16.2.10 | React framework: App Router, server components, route handlers, next/image |
react | ^19.2.7 | UI runtime |
react-dom | ^19.2.7 | DOM renderer |
typescript | ^6.0.3 | Static typing across the codebase (tsc --noEmit in CI) |
tailwindcss | ^4.3.2 | Utility-first CSS, v4 engine |
@tailwindcss/postcss | ^4.3.2 | The v4 PostCSS plugin (v4 no longer registers tailwindcss itself) |
tailwindcss-animate | ^1.0.7 | Animation utilities used by the UI kit |
tailwind-merge | ^3.6.0 | Resolves conflicting Tailwind classes; backs the cn() helper |
clsx | ^2.1.1 | Conditional class names |
class-variance-authority | ^0.7.1 | Typed component variants |
zod | ^4.4.3 | Runtime schema validation (forms, API payloads, email payloads) |
react-hook-form | ^7.81.0 | Form state and validation |
@hookform/resolvers | ^5.4.0 | Bridges Zod schemas into react-hook-form |
framer-motion | ^12.42.2 | Animation |
lucide-react | ^1.23.0 | Icons |
date-fns | ^4.4.0 | Date formatting |
Why the App Router
Server components fetch the catalog and resolve locale, currency, and theme before the first paint, so prices and language are right in the initial HTML instead of being corrected after hydration. Product and category pages carry their own generateMetadata and JSON-LD. Route handlers under src/app/api/* serve the public Store API, the checkout endpoint, and the commerce RPC bridge.
Data layer
The commerce layer (src/lib/commerce) defines one typed interface, CommerceServices, and every adapter satisfies it. Switching is an env var, not a fork; see choosing a backend.
PostgreSQL via Prisma (recommended), and SQLite
Your own database, standard SQL, no vendor lock-in. The adapter is server-side only: the browser reaches it through the /api/commerce RPC route, so the connection string never leaves the server.
SQLite is the same adapter. One CommerceServices implementation over one set of models; only three statements differ by engine, all documented on the SQLite page. It is the shortest path to real writes while developing — a file, nothing to install — and moving to Postgres later is two env vars.
| Package | Version | Role |
|---|---|---|
@prisma/client | ^7.8.0 | Runtime client |
prisma | ^7.8.0 | CLI and schema tooling (dev dependency) |
@prisma/adapter-pg | ^7.8.0 | Driver adapter binding Prisma to pg |
pg | ^8.22.0 | PostgreSQL driver |
@prisma/adapter-better-sqlite3 | ^7.9.0 | Driver adapter for the SQLite backend |
better-sqlite3 | ^12.11.1 | SQLite driver (native addon, loaded only on that branch) |
# .env.local — Postgres
DATABASE_URL="postgresql://user:password@localhost:5432/store"
COMMERCE_BACKEND=postgres
NEXT_PUBLIC_COMMERCE_BACKEND=postgres
npm run prisma:generate
npm run prisma:push# .env.local — SQLite (COMMERCE_BACKEND also picks the schema file, so pass it)
DATABASE_URL="file:./dev.db"
COMMERCE_BACKEND=sqlite
NEXT_PUBLIC_COMMERCE_BACKEND=sqlite
COMMERCE_BACKEND=sqlite npm run prisma:generate
COMMERCE_BACKEND=sqlite npm run prisma:pushSet a plain postgresql:// URL before running prisma generate. With no URL the Prisma CLI drops into its hosted-database onboarding and asks for an API key; this project never needs a Prisma account.
Setup in full: Postgres and Prisma, SQLite.
Supabase
Hosted Postgres with row-level security, behind the same adapter family. Set COMMERCE_BACKEND=supabase plus SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (server-side only, never prefixed NEXT_PUBLIC_).
| Package | Version | Role |
|---|---|---|
@supabase/supabase-js | ^2.109.0 | Supabase client, used by the server adapter |
Firebase
Firestore runs client-side, which buys realtime carts and a zero-SQL setup, and Firebase Auth is the default identity provider on every backend. Admin mutations are routed through the server so they can be authorized and audited there.
| Package | Version | Role |
|---|---|---|
firebase | ^12.15.0 | Client SDK: Auth (Google, passwordless email link), Firestore, Storage |
firebase-admin | ^14.1.0 | Server SDK: verifies ID tokens, records orders from the Stripe webhook, performs audited admin writes, powers the seed script |
react-firebase-hooks | ^5.1.1 | React bindings for auth state |
Identity, when it is not Firebase
src/lib/auth is the seam: one function turns a request into { uid, email }, and AUTH_PROVIDER picks who implements it. authjs keeps sessions as rows in the SQL backend you already run — a session that can actually be revoked, and no Firebase project. Both packages load lazily, so a Firebase deployment never pulls them in.
| Package | Version | Role |
|---|---|---|
next-auth | ^5.0.0-beta.32 | Auth.js v5: Google OAuth and Resend magic links, database session strategy |
@auth/prisma-adapter | ^2.11.3 | Stores Auth.js users, accounts, and sessions through the same Prisma client |
Setup and the reasoning behind failing closed: authentication.
The demo backend
No package at all: src/lib/commerce/demo serves catalog.json, the same 18 products npm run seed writes into a real backend. It is what makes npm run dev show a populated store on a fresh clone, and what lets CI run a production build with no credentials.
UI kit
The interface is built from shadcn/ui components copied into src/components/ui/, wrapping Radix primitives for accessible behaviour and styled with Tailwind. They live in the repo, so you own and edit them.
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": { "components": "@/components", "utils": "@/lib/utils" }
}Radix ships one package per primitive; the full list is in package.json (accordion, alert-dialog, aspect-ratio, avatar, checkbox, collapsible, dialog, dropdown-menu, label, menubar, navigation-menu, popover, radio-group, scroll-area, select, separator, slot, switch, tabs, toast, tooltip). Two related helpers ship alongside them:
| Package | Version | Role |
|---|---|---|
vaul | ^1.1.2 | Drawer and mobile sheet |
react-day-picker | ^10.0.1 | Calendar and date picker |
There is deliberately no input-masking package. react-input-mask reached the DOM through ReactDOM.findDOMNode in componentDidMount, which React 19 removed, so every form with a phone field threw on mount and fell to its error boundary — taking the guest checkout dialog with it. src/lib/phone-mask.ts replaces it with a pure function the mask and its validation pattern are both derived from, which is why they cannot drift apart and why the behaviour is testable without a DOM.
Fonts
Two families, loaded in src/app/[locale]/layout.tsx and exposed as CSS variables the Tailwind config reads.
| Family | Source | Variable | Utility |
|---|---|---|---|
| Bricolage Grotesque | next/font/google | --font-display | font-display |
| Geist Sans | geist/font/sans | --font-sans | font-sans (body default) |
| Geist Mono | geist/font/mono | --font-mono | font-mono |
| Package | Version | Role |
|---|---|---|
geist | ^1.7.2 | Geist Sans and Geist Mono, self-hosted |
Geist comes from its own package rather than next/font/google, and globals.css aliases its variables onto the design tokens.
Payments
| Package | Version | Role |
|---|---|---|
stripe | ^22.3.0 | Server-side SDK for Checkout Sessions and webhook signature verification |
With STRIPE_SECRET_KEY set, a priced cart gets a Checkout button. POST /api/checkout re-reads every product and variant server-side and prices the session in the store's base currency, so nothing the browser sends can move an amount. POST /api/webhooks/stripe verifies the signature and records the order through the active backend — Postgres, Supabase, or Firestore — keyed by the session id so a retried delivery cannot duplicate it, and consuming the stock it sold in the same transaction. See payments.
Upgrading a SQL store? orders.payment_session_id and payment_status are new columns. Run npm run prisma:push, or re-run supabase/schema.sql, before the first paid webhook arrives.
| Package | Version | Role |
|---|---|---|
resend | ^6.17.1 | Sends transactional email |
react-email | ^6.6.6 | Local preview tooling |
@react-email/components | ^1.0.12 | Components for building the templates |
Without RESEND_API_KEY the store runs normally and skips email. See email notifications.
Documentation
This site is Fumadocs, compiled by a postinstall step.
| Package | Version | Role |
|---|---|---|
fumadocs-ui | ^16.10.7 | Docs shell and components |
fumadocs-core | ^16.10.7 | Routing and search primitives |
fumadocs-mdx | ^15.0.13 | MDX content pipeline |
@types/mdx | ^2.0.14 | MDX types |
Testing and tooling
| Package | Version | Role |
|---|---|---|
vitest | ^4.1.9 | Test runner |
@playwright/test | ^1.62.1 | End-to-end runner, Chromium only |
tsx | ^4.23.1 | Runs the TypeScript seed script and the e2e provisioning scripts against the real commerce adapter |
eslint | ^9.39.4 | Linting (flat config) |
eslint-config-next | ^16.2.10 | Next's rule set |
postcss | ^8 | Drives the Tailwind build |
Three suites, none of which needs a credential, and each of which can prove something the one before it cannot: Testing covers what each is for and which to run.
These docs are part of what the first suite checks — docs-versions.test.ts is why the version in every row above matches package.json, and it fails the build when it does not.
| Command | Runs | Purpose |
|---|---|---|
npm run dev | next dev | Development server |
npm run build | next build | Production build |
npm run start | next start | Serve the build |
npm run lint | eslint . | Lint (CI runs it with --max-warnings 0) |
npm run typecheck | tsc --noEmit | Type-check |
npm test | vitest run | Unit suite |
npm run test:watch | vitest | Watch mode |
npm run test:sqlite | prisma generate then Vitest | Drives the SQL adapter against a real SQLite database |
npm run test:e2e | provision, build, playwright test | Drives a browser against a built store |
npm run seed | tsx scripts/seed.ts | Seed the demo catalog into whichever backend is configured |
npm run prisma:generate | prisma generate | Generate the Prisma client |
npm run prisma:push | prisma db push | Push the schema to DATABASE_URL |
package.json is the authority on all of them, including the e2e:* steps test:e2e composes.