Project Structure
An annotated tour of the repository layout and where each concern lives.
This page is a map. It walks the repository top-down, then dives into src/,
so you know exactly where to reach when you want to change branding, add an API
route, swap the commerce backend, or add a language. Every path below is real —
open them alongside this page.
Top level
ecommerce-boilerplate/
├─ content/docs/ # This documentation (Fumadocs MDX source)
├─ docs/ # Backend adapter guides + CI recipe (plain markdown)
│ ├─ adapters/
│ │ ├─ postgres-prisma.md
│ │ └─ supabase.md
│ └─ ci.example.yml
├─ src/ # All application code (see below)
├─ public/ # Static assets (category SVGs, favicon, images)
├─ prisma/
│ └─ schema.prisma # Postgres/Prisma backend schema (optional)
├─ supabase/
│ └─ schema.sql # Supabase (Postgres) backend schema (optional)
├─ scripts/
│ └─ seed.mjs # Idempotent demo-data seeding
├─ tests/ # Vitest unit suite (see "tests/" below)
├─ firestore.rules # Firestore security rules (admin/role enforcement)
├─ storage.rules # Firebase Storage security rules
├─ next.config.mjs # Next.js config + Fumadocs MDX plugin
├─ tailwind.config.ts # Tailwind theme (reads the --brand* design tokens)
├─ source.config.ts # Fumadocs content-source config
├─ prisma.config.ts # Prisma CLI config
├─ postcss.config.mjs
├─ components.json # shadcn/ui generator config
├─ vitest.config.ts
├─ tsconfig.json
├─ README.md
├─ ROADMAP.md
└─ SECURITY.mdThe store is quote-based by default: browse → cart → submit a quote request,
with no payment step. prisma/, supabase/, and the Stripe/Resend wiring are
all optional and environment-gated — a default build runs on Firebase alone
and never needs them. See environment variables.
The src/ tree
src/
├─ app/ # Next.js App Router
│ ├─ [locale]/ # Locale-prefixed storefront + admin (de-facto root layout)
│ ├─ api/ # Route handlers (server endpoints)
│ ├─ docs/ # Renders /docs from content/docs (NOT locale-prefixed)
│ ├─ globals.css # Design tokens + Tailwind layers
│ ├─ robots.ts # /robots.txt
│ ├─ sitemap.ts # /sitemap.xml
│ └─ favicon.ico
├─ components/ # React components, grouped by surface (see below)
├─ config/
│ └─ site.ts # Single-file branding: name, contact, nav, currencies…
├─ contexts/ # Client React context providers (cart, user)
├─ hooks/ # Reusable client hooks
├─ lib/ # Framework-agnostic logic (see "src/lib" below)
├─ types/ # Shared domain types (product, quote, customer…)
└─ middleware.ts # Locale-prefix redirect (edge runtime)src/app/[locale] — the routed app and de-facto root layout
Every storefront and admin page lives under a [locale] segment, so URLs are
locale-prefixed (/en/products, /es/cart). There is no src/app/layout.tsx —
so src/app/[locale]/layout.tsx is the root layout. It owns <html lang> and
<body>, mounts every top-level provider, and renders the shared Header,
Footer, and Toaster:
return (
<html lang={locale}>
<body className={`${display.variable} ${GeistSans.variable} ${GeistMono.variable} font-sans`}>
<ThemeStyle />
<OrganizationJsonLd />
<UserProvider>
<LocaleProvider initialLocale={locale}>
<CurrencyProvider initialCurrency={currency}>
<CartProvider>
<Header />
{children}
<Toaster />
<Footer locale={locale} />
</CartProvider>
</CurrencyProvider>
</LocaleProvider>
</UserProvider>
</body>
</html>
);generateStaticParams() pre-renders one route tree per configured locale, and an
unknown prefix (/fr/...) calls notFound() instead of silently falling back.
The page segments under [locale]:
| Segment | Route | Purpose |
|---|---|---|
page.tsx | /{locale} | Landing page (hero, categories, featured, contact) |
products/ | /{locale}/products | Catalog + product detail pages |
cart/ | /{locale}/cart | Cart review |
checkout/ | /{locale}/checkout | Quote-request (and optional Stripe) checkout |
login/ | /{locale}/login | Google + email-link sign-in |
complete-login/ | /{locale}/complete-login | Passwordless email-link landing |
account/ | /{locale}/account | Profile, addresses, quote/order history |
admin/ | /{locale}/admin | Admin dashboard (role-gated) |
about/ | /{locale}/about | About page |
privacy-policy/, terms-of-service/, cookie-policy/ | /{locale}/… | Legal pages |
src/app/api — route handlers
Server endpoints, one folder per resource. See the API reference for full request/response detail.
api/
├─ store/ # Public read-only catalog API
│ ├─ route.ts # GET /api/store
│ ├─ categories/route.ts # GET /api/store/categories
│ └─ products/
│ ├─ route.ts # GET /api/store/products
│ └─ [id]/route.ts # GET /api/store/products/:id
├─ commerce/[service]/[method]/route.ts # POST RPC layer for SQL backends
├─ checkout/
│ ├─ route.ts # POST /api/checkout (Stripe, optional)
│ └─ config/route.ts # GET /api/checkout/config
├─ webhooks/stripe/route.ts # Stripe webhook (optional)
├─ quotes/notify/route.ts # Quote email notifications (optional)
└─ notifications/status/route.ts # Email-integration status probeThe /api/store/* catalog endpoints are always available (rate-limited, cache
headers). The commerce/[service]/[method] route is the RPC layer used only
when a SQL backend is selected — see commerce backends.
The checkout, webhooks, and quotes routes are env-gated.
src/app/docs — this documentation
src/app/docs/[[...slug]]/page.tsx and src/app/docs/layout.tsx render the
Fumadocs site from the MDX in content/docs/. This subtree is deliberately not
locale-prefixed — the middleware excludes /docs, /api, and
static files from the locale redirect. The MDX is compiled at build time by the
Fumadocs plugin wired into next.config.mjs:
const withMDX = createMDX();
export default withMDX(nextConfig);src/components
Presentational and interactive components, grouped by the surface they serve:
| Folder | What lives here |
|---|---|
home/ | Landing sections plus the shared Header and Footer |
product/ | Product cards, galleries, detail views |
cart/ | Cart drawer, line items |
admin/ | Admin dashboard UI |
login/, userProfile/ | Auth and account UI |
header/, preferences/ | Header controls and preference menus |
i18n/, currency/, theme/ | Locale / currency / theme switchers (ThemeStyle injects --brand*) |
emailing/, legal/, seo/ | Newsletter/contact, legal copy, JSON-LD (OrganizationJsonLd) |
ui/ | 30+ shadcn/ui (Radix) primitives — buttons, dialogs, toaster, etc. |
src/lib
Framework-agnostic logic — the parts you can unit-test without a browser. The subdirectories are the important seams:
lib/
├─ commerce/ # Backend-agnostic commerce service layer (see below)
├─ firebase/ # Firebase SDK wiring: auth, Firestore, Storage, admin
├─ i18n/ # Dictionaries + locale resolution
├─ currency/ # Currency provider + server-side active-currency resolution
├─ theme/ # Brand presets + server-side active-theme resolution
├─ api/ # store.ts — helpers behind the public /api/store endpoints
├─ money.ts # formatMoney — the single price-formatting seam
├─ cart.ts, order-total.ts, checkout-line.ts, checkout-metadata.ts
├─ inventory.ts, discounts.ts # Stock + discount logic
├─ email.ts, stripe.ts # Optional Resend / Stripe helpers
├─ audit.ts, rate-limit.ts # Cross-cutting concerns
├─ motion.ts, utils.ts # Animation + misc helpers
└─ source.ts # Fumadocs content source loadersrc/lib/commerce — the backend-agnostic service layer
The single seam between the app and its commerce backend. Every adapter satisfies
the same CommerceServices interface and emits the same lifecycle events, so
swapping backends is a data/config change, not an app rewrite.
| File | Role |
|---|---|
types.ts | Backend-neutral interfaces: ProductService, CartService, QuoteService, OrderService, DiscountService, CustomerService, AdminService (grouped as CommerceServices) |
firebase.ts | Firebase adapter — delegates to FirebaseDBService, runs client-side |
http.ts | Client HTTP adapter — forwards each call to the /api/commerce RPC route (used with SQL backends) |
server/index.ts | getServerCommerce() — resolves the SQL adapter from COMMERCE_BACKEND |
server/supabase.ts | Supabase (Postgres) adapter — server-side, service-role key |
server/prisma.ts | Postgres/Prisma adapter — server-side, direct DATABASE_URL |
events.ts | Typed, dependency-free event bus (on, once, emit) |
index.ts | Binds commerce to the active adapter and re-exports everything |
Read the full contract in commerce backends overview and the commerce RPC and events pages.
src/lib/firebase
Firebase SDK wiring. config.ts reads the NEXT_PUBLIC_FIREBASE_* env vars;
authService.ts handles Google + passwordless email-link sign-in; dbService.ts
(FirebaseDBService) is the Firestore data layer; storageService.ts handles
uploads; admin.ts is the server-side firebase-admin instance used to verify ID
tokens in the RPC route; firebaseDataConverters.ts and firebaseUtils.ts hold
typed converters and helpers.
src/lib/i18n
Locale dictionaries and resolution. en.ts and es.ts are the shipped keyed
dictionaries; index.ts registers them and exposes resolveLocale; server.ts
resolves server-side; LocaleProvider.ts is the client provider; navigation.ts
builds locale-aware links. Adding a language is a data change: drop
src/lib/i18n/<code>.ts and register it. See internationalization.
src/lib/currency
CurrencyProvider.tsx is the client provider; server.ts exposes
getActiveCurrency() (read in the root layout) so the first server paint already
reflects the shopper's cookie-selected currency. Prices are stored in the base
currency and converted for display by formatMoney in src/lib/money.ts. See
currency.
src/lib/theme
presets.ts holds the brand color presets; server.ts resolves the active
preset server-side. The <ThemeStyle /> component (src/components/theme)
injects the active preset's --brand* CSS variables. See
branding & theming.
src/config/site.ts — single-file branding
The single place to rebrand the boilerplate: store name, tagline,
description, url, contact, social links, mainNav / legalNav,
currency and currencies, defaultLocale / locales, the theme presets,
and the exported productCategories array. Components read from this config
instead of hardcoding copy.
export const siteConfig = {
name: "Acme Store",
tagline: "Everything you need to launch your online store",
currency: { code: "USD", locale: "en-US" },
defaultLocale: "en",
locales: [
{ code: "en", label: "English" },
{ code: "es", label: "Español" },
],
// …contact, links, mainNav, theme presets, etc.
} as const;Full field-by-field reference: site configuration.
src/contexts and src/hooks
src/contexts holds the two client providers mounted in the root layout:
cartContext.tsx (CartProvider) and userContext.tsx (UserProvider).
src/hooks holds reusable client hooks such as useIsAdmin.ts.
src/types
Shared domain types consumed across the app: product.ts, quote.ts,
customer.ts, and personalInfo.ts.
Optional backend directories
These exist only for the optional SQL backends and are ignored by a default
Firebase build (their packages are kept external in next.config.mjs):
| Path | Used by |
|---|---|
prisma/schema.prisma | Postgres/Prisma adapter (COMMERCE_BACKEND=postgres) |
supabase/schema.sql | Supabase adapter (COMMERCE_BACKEND=supabase) |
docs/adapters/postgres-prisma.md, docs/adapters/supabase.md | Human setup guides for each |
Security rules
Two rule files ship at the repository root and enforce access on the default Firebase backend:
firestore.rules— Firestore access control, including theadminsrole collection that gates the admin dashboard.storage.rules— Firebase Storage access control for product image uploads.
middleware.ts
src/middleware.ts makes the locale prefix mandatory. Unprefixed requests
(/, /products, …) are 307-redirected to the visitor's active locale,
resolved in this order: the locale cookie → the best match from the
Accept-Language header → siteConfig.defaultLocale. It runs on the edge and is
kept dependency-free.
export const config = {
matcher: [
"/((?!api|docs|_next|_vercel|.*\\..*|sitemap\\.xml|robots\\.txt|favicon\\.ico).*)",
],
};The matcher excludes api, the docs subtree, Next/Vercel internals, any path
with a file extension, and the SEO/meta files — which is why /docs and
/api/* are never locale-prefixed.
tests/
A flat Vitest unit suite covering the framework-agnostic src/lib logic and the
design system — for example money.test.ts, discounts.test.ts,
inventory.test.ts, order-total.test.ts, i18n.test.ts, theme.test.ts,
commerce-surface.test.ts, and button-variants.test.ts. Run them with npm test.