Deployment
Deploy to Vercel with the right environment variables and production settings.
The boilerplate works out of the box on Vercel: import the repo, set the environment variables from .env.example, and deploy. Any Node host that supports Next.js 16 works too. Stripe payments, Resend emails, and the database adapters are all optional and env-gated — a first deploy needs nothing more than your Firebase web credentials and a sign-in redirect URL, and without Stripe the store is quote-based.
SQLite is the one backend you cannot deploy this way. Serverless filesystems are ephemeral and per-instance, so the database would vanish between deploys and never be shared between instances. Use it locally, deploy on Postgres — the same adapter, two env vars apart. See SQLite.
A default (Firebase) build succeeds with no credentials at all: src/lib/firebase/config.ts falls back to demo values so the SDK initializes during prerender. You still need real env vars at runtime for the deployed store to talk to your own Firebase project.
Deploy to Vercel
Import the repository
In the Vercel dashboard, Add New → Project and import your fork/clone. Vercel auto-detects Next.js (App Router) — the default build command next build and output settings need no changes.
Set the environment variables
Open Project Settings → Environment Variables and add every value your deployment needs from .env.example. For a default quote-based store, the Firebase web credentials plus the sign-in redirect URL are the only required entries:
| Variable | Required | Notes |
|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | Yes | Firebase web app config |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | e.g. your-project.firebaseapp.com |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | e.g. your-project |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | Yes* | Required for product image uploads / media library |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | Yes | Firebase web app config |
NEXT_PUBLIC_FIREBASE_APP_ID | Yes | Firebase web app config |
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID | No | Analytics, optional |
NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URL | Yes | Set to your production /complete-login URL (see below) |
NEXT_PUBLIC_* values are compiled into the browser bundle. Firebase web credentials are designed to be public — enforce access control with firestore.rules, not by hiding the keys.
Point the sign-in redirect at production
Passwordless email-link sign-in redirects back to the URL in NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URL. In development this is http://localhost:3000/complete-login; in production set it to your deployed origin plus /complete-login:
NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URL=https://shop.example.com/complete-loginAuthorize the production domain in Firebase
The redirect domain must be listed in Firebase console → Authentication → Settings → Authorized domains. Add your production hostname (e.g. shop.example.com, and any Vercel preview domain you want sign-in to work on) or the email-link flow is rejected.
Add optional integrations (if used)
Only set these when you actually want the feature — leaving them unset keeps the store quote-only:
| Feature | Env vars | Effect when unset |
|---|---|---|
| Stripe Checkout | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | Checkout button never appears |
| Order recording from webhook | FIREBASE_SERVICE_ACCOUNT_JSON (or GOOGLE_APPLICATION_CREDENTIALS/FIREBASE_SERVICE_ACCOUNT path) | Webhook acknowledges events but can't write orders |
| Email notifications | RESEND_API_KEY, RESEND_FROM_EMAIL, QUOTE_NOTIFICATION_EMAIL | Emails skipped, store works normally |
| SQL backend | NEXT_PUBLIC_COMMERCE_BACKEND + COMMERCE_BACKEND (+ SUPABASE_* or DATABASE_URL) | Default Firebase backend runs client-side |
Deploy
Trigger the deploy. On success, visit the site, sign in once, and (if you want the admin dashboard) add your user id to the Firestore admins collection.
The Stripe webhook needs a service account
Stripe order recording happens server-side with the Firebase Admin SDK, which needs privileged credentials that the browser never sees. On Vercel the convenient option is FIREBASE_SERVICE_ACCOUNT_JSON — paste the service-account key's JSON contents (not a file path) directly into the environment variable:
Generate a key
Firebase console → Project settings → Service accounts → Generate new private key. This downloads a JSON file. The file is git-ignored — never commit it.
Paste the contents into Vercel
Copy the whole JSON object and paste it as the value of FIREBASE_SERVICE_ACCOUNT_JSON:
FIREBASE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"your-project", ... }Alternatively, provide a key-file path via GOOGLE_APPLICATION_CREDENTIALS or FIREBASE_SERVICE_ACCOUNT — but a pasted JSON blob is simpler on serverless hosts where there is no persistent filesystem.
Register the webhook endpoint
In the Stripe dashboard, add a webhook pointing at https://your-domain/api/webhooks/stripe and copy its signing secret into STRIPE_WEBHOOK_SECRET.
Without admin credentials the POST /api/webhooks/stripe handler still acknowledges events (so Stripe won't retry-storm) but logs that the order could not be recorded. If paid orders aren't showing up in /admin/orders, check that FIREBASE_SERVICE_ACCOUNT_JSON is set on the deployment.
The same admin credentials also power the npm run seed demo-data script — see Demo data. Note the SQL backends require FIREBASE_SERVICE_ACCOUNT* too, because Firebase Auth stays the identity provider and its ID tokens are verified server-side.
Configure images for your CDN
Product images (primaryImageURL) render through next/image, so every host they can be served from must be listed in images.remotePatterns in next.config.mjs, or next/image refuses to optimize those URLs. Firebase Storage and the demo photography host ship allowed, so a store that uploads its images needs no change here; add your own CDN or bucket hostname before deploying. Media & image uploads has the config block and a worked example.
next.config.mjs also lists @prisma/client, @prisma/adapter-pg, pg, @prisma/adapter-better-sqlite3, and better-sqlite3 under serverExternalPackages. This keeps the optional SQL-backend packages out of the bundle so a default build succeeds without them installed — you don't need Prisma, Postgres, or SQLite for a standard deploy. (better-sqlite3 is a native addon on top of that: bundling it would break .node binding resolution outright.)
Continuous integration
The repository ships its own GitHub Actions workflow at .github/workflows/ci.yml. It runs on pushes to main and on every pull request, cancels superseded runs on the same branch, and splits into two jobs that run in parallel, both on Node 20.
The verify job is the gate — lint, typecheck, build, unit tests, SQLite integration tests:
- name: Lint
# --max-warnings 0 — warnings are failures here.
run: npm run lint -- --max-warnings 0
- name: Typecheck
run: npm run typecheck
- name: Build
run: npm run build
# No env needed: with no credentials the commerce layer resolves to the
# bundled demo backend and src/lib/firebase/config.ts falls back to
# placeholder values, so the build never needs a real project.
- name: Test
run: npm test
- name: Test (SQLite integration)
run: npm run test:sqliteTwo orderings there are deliberate, not accidental:
- Build runs before Test.
tests/api-routes-dynamic.test.tsasserts that every API route stayed dynamic by reading the prerender manifest — a build artifact. Run the tests first and that suite silently skips itself. - The SQLite integration suite runs last. It generates a Prisma client, and the build above has to prove the project builds without one.
The build step needs no environment variables. The optional SQL packages are external, so the default build never requires @prisma/client, pg, better-sqlite3, or a DATABASE_URL.
The e2e job drives Playwright against a production build:
- name: Install Playwright (chromium)
run: npx playwright install --with-deps chromium
- name: Run end-to-end tests
run: npm run test:e2eIt is a separate job so that a browser install and a second production build cannot slow down — or fail — the gate above, and so the two run at the same time rather than in sequence. Chromium only: the specs guard flows, not rendering engines, and a second browser would double the minutes to re-verify the same server-side behaviour. It needs no credentials either — COMMERCE_BACKEND=sqlite puts the store on a file database and AUTH_PROVIDER=authjs keeps sessions in it, so there is no Firebase project and no Stripe account in the loop (e2e/env.ts holds the whole environment, e2e/scripts/provision.ts seeds the catalog and accounts). npm run test:e2e provisions before it builds, because next build reads the catalog. The HTML report uploads on every run; screenshots, video, and traces only on a failure.
For what each suite can actually prove, and which one to reach for on a given change, see Testing.
Forking? The workflow is already in place — nothing to copy. If you add deployment steps, keep the Build-before-Test order or you lose the dynamic-route check.
Production readiness
Before going live, don't ship the demo defaults straight to production:
- Wire a live FX feed. Currency conversion uses demo rates baked into
siteConfig.currencies. Prices are stored in the base currency (USD) and converted at display time — replace the demo rates with a real feed for accurate customer-facing prices. - Deploy and verify
firestore.rules. The shipped rules enforce all access control (admin gating, append-only audit log, public catalog reads). Deploy them withfirebase deploy --only firestore:rulesand confirm they're active — the UIuseIsAdmincheck is convenience only. If you use image uploads, deploystorage.rulestoo (firebase deploy --only storage). - Use a real Resend sender.
onboarding@resend.devis for testing only. SetRESEND_FROM_EMAILto a domain you've verified in Resend before enabling email notifications. - Review the legal pages. The privacy/terms/cookie pages under
src/app/are placeholders — review them with counsel.