Ecommerce Boilerplate
Getting Started

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.

CapabilityTurns on when you setWithout 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_URLAs above
Sign-in, saved carts, order historyNEXT_PUBLIC_FIREBASE_* (plus a service account for server writes), or AUTH_PROVIDER=authjs on a SQL backendBrowsing and the cart work; anything per-customer is unavailable
Card paymentsSTRIPE_SECRET_KEY (plus STRIPE_WEBHOOK_SECRET to record orders)Demo checkout confirms locally; a configured store falls back to the quote flow
Transactional emailRESEND_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.

PackageVersionRole
next^16.2.10React framework: App Router, server components, route handlers, next/image
react^19.2.7UI runtime
react-dom^19.2.7DOM renderer
typescript^6.0.3Static typing across the codebase (tsc --noEmit in CI)
tailwindcss^4.3.2Utility-first CSS, v4 engine
@tailwindcss/postcss^4.3.2The v4 PostCSS plugin (v4 no longer registers tailwindcss itself)
tailwindcss-animate^1.0.7Animation utilities used by the UI kit
tailwind-merge^3.6.0Resolves conflicting Tailwind classes; backs the cn() helper
clsx^2.1.1Conditional class names
class-variance-authority^0.7.1Typed component variants
zod^4.4.3Runtime schema validation (forms, API payloads, email payloads)
react-hook-form^7.81.0Form state and validation
@hookform/resolvers^5.4.0Bridges Zod schemas into react-hook-form
framer-motion^12.42.2Animation
lucide-react^1.23.0Icons
date-fns^4.4.0Date 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.

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.

PackageVersionRole
@prisma/client^7.8.0Runtime client
prisma^7.8.0CLI and schema tooling (dev dependency)
@prisma/adapter-pg^7.8.0Driver adapter binding Prisma to pg
pg^8.22.0PostgreSQL driver
@prisma/adapter-better-sqlite3^7.9.0Driver adapter for the SQLite backend
better-sqlite3^12.11.1SQLite driver (native addon, loaded only on that branch)
Terminal
# .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
Terminal
# .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:push

Set 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_).

PackageVersionRole
@supabase/supabase-js^2.109.0Supabase 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.

PackageVersionRole
firebase^12.15.0Client SDK: Auth (Google, passwordless email link), Firestore, Storage
firebase-admin^14.1.0Server SDK: verifies ID tokens, records orders from the Stripe webhook, performs audited admin writes, powers the seed script
react-firebase-hooks^5.1.1React 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.

PackageVersionRole
next-auth^5.0.0-beta.32Auth.js v5: Google OAuth and Resend magic links, database session strategy
@auth/prisma-adapter^2.11.3Stores 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.

components.json
{
  "$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:

PackageVersionRole
vaul^1.1.2Drawer and mobile sheet
react-day-picker^10.0.1Calendar 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.

FamilySourceVariableUtility
Bricolage Grotesquenext/font/google--font-displayfont-display
Geist Sansgeist/font/sans--font-sansfont-sans (body default)
Geist Monogeist/font/mono--font-monofont-mono
PackageVersionRole
geist^1.7.2Geist 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

PackageVersionRole
stripe^22.3.0Server-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.

Email

PackageVersionRole
resend^6.17.1Sends transactional email
react-email^6.6.6Local preview tooling
@react-email/components^1.0.12Components 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.

PackageVersionRole
fumadocs-ui^16.10.7Docs shell and components
fumadocs-core^16.10.7Routing and search primitives
fumadocs-mdx^15.0.13MDX content pipeline
@types/mdx^2.0.14MDX types

Testing and tooling

PackageVersionRole
vitest^4.1.9Test runner
@playwright/test^1.62.1End-to-end runner, Chromium only
tsx^4.23.1Runs the TypeScript seed script and the e2e provisioning scripts against the real commerce adapter
eslint^9.39.4Linting (flat config)
eslint-config-next^16.2.10Next's rule set
postcss^8Drives 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.

CommandRunsPurpose
npm run devnext devDevelopment server
npm run buildnext buildProduction build
npm run startnext startServe the build
npm run linteslint .Lint (CI runs it with --max-warnings 0)
npm run typechecktsc --noEmitType-check
npm testvitest runUnit suite
npm run test:watchvitestWatch mode
npm run test:sqliteprisma generate then VitestDrives the SQL adapter against a real SQLite database
npm run test:e2eprovision, build, playwright testDrives a browser against a built store
npm run seedtsx scripts/seed.tsSeed the demo catalog into whichever backend is configured
npm run prisma:generateprisma generateGenerate the Prisma client
npm run prisma:pushprisma db pushPush the schema to DATABASE_URL

package.json is the authority on all of them, including the e2e:* steps test:e2e composes.

Next steps

On this page