Ecommerce Boilerplate
Configuration

Environment Variables

Complete reference for every environment variable and the feature it controls.

The boilerplate is configured entirely through environment variables. Copy the template and fill in the values you need:

Terminal
cp .env.example .env.local

Nothing here is required to boot. With no NEXT_PUBLIC_FIREBASE_PROJECT_ID and no backend named, the storefront serves the built-in demo catalog, so a fresh clone renders a populated store before any setup — see commerce backend selection. The Firebase web credentials and the email sign-in redirect URL are what the default real deployment needs. Everything else is optional and feature-gated: leaving an optional variable empty disables that feature gracefully, with no crash and no build failure. The store's default mode is quote-based (browse → cart → submit a quote request), so payments, transactional email, and SQL backends all stay off until you opt in.

Variables prefixed with NEXT_PUBLIC_ are inlined into the browser bundle at build time and are public by design. Never give that prefix to a secret. All other variables are server-only and never reach the client.

Quick reference

"Yes" means required for the default Firebase deployment; the demo backend needs none of them.

VariableRequiredFeature
NEXT_PUBLIC_FIREBASE_API_KEYYesFirebase / Auth / Firestore
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYesFirebase Auth
NEXT_PUBLIC_FIREBASE_PROJECT_IDYesFirebase project
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKETYesFirebase Storage
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_IDYesFirebase
NEXT_PUBLIC_FIREBASE_APP_IDYesFirebase
NEXT_PUBLIC_FIREBASE_MEASUREMENT_IDOptionalFirebase Analytics
NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URLYesPasswordless email sign-in
RESEND_API_KEYOptionalQuote/status email notifications
RESEND_FROM_EMAILOptionalEmail sender address
QUOTE_NOTIFICATION_EMAILOptionalNew-quote notification inbox
FIREBASE_SERVICE_ACCOUNTOptionalnpm run seed, server-side Firestore
STRIPE_SECRET_KEYOptionalStripe Checkout
STRIPE_WEBHOOK_SECRETOptionalStripe webhook verification
FIREBASE_SERVICE_ACCOUNT_JSONOptionalWebhook / server-side Firestore writes
UPSTASH_REDIS_REST_URLOptionalShared rate-limit store
UPSTASH_REDIS_REST_TOKENOptionalShared rate-limit store
AUTH_PROVIDEROptionalServer-side identity provider (default firebase)
NEXT_PUBLIC_AUTH_PROVIDEROptionalBrowser-side identity provider (must match)
AUTH_SECRETWith Auth.jsAuth.js session-signing secret
AUTH_GOOGLE_IDOptionalAuth.js Google sign-in
AUTH_GOOGLE_SECRETOptionalAuth.js Google sign-in
NEWSLETTER_PROVIDEROptionalWhere newsletter signups are stored
NEXT_PUBLIC_COMMERCE_BACKENDOptionalBrowser commerce adapter (demo, firebase, sqlite, postgres, supabase)
COMMERCE_BACKENDOptionalServer commerce adapter
NEXT_PUBLIC_ADMIN_WRITESOptionalOpt out of server-routed admin writes (Firebase)
SUPABASE_URLOptionalSupabase backend
SUPABASE_SERVICE_ROLE_KEYOptionalSupabase backend
DATABASE_URLOptionalPostgres/Prisma backend

Firebase web credentials

Firebase Auth is the default identity provider, and on a Firestore data backend the only one (firestore.rules gate on request.auth). On a SQL backend you can switch to Auth.js and drop Firebase entirely — see authentication. Firebase is also the commerce backend that takes over implicitly once a project id is set and no backend is named; every other backend is opt-in. These seven values come from the Firebase console → Project settings → Your apps. They are NEXT_PUBLIC_* because Firebase web credentials are meant to be public — access control lives in your Firestore security rules, not in these keys.

VariableRequiredExampleDescription
NEXT_PUBLIC_FIREBASE_API_KEYYes(from console)Web API key for the Firebase JS SDK.
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYesyour-project.firebaseapp.comAuth domain used by the sign-in flow.
NEXT_PUBLIC_FIREBASE_PROJECT_IDYesyour-projectFirebase / Google Cloud project ID.
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKETYesyour-project.appspot.comStorage bucket for uploaded assets.
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_IDYes(from console)Cloud Messaging sender ID.
NEXT_PUBLIC_FIREBASE_APP_IDYes(from console)Firebase web app ID.
NEXT_PUBLIC_FIREBASE_MEASUREMENT_IDOptional(from console)Google Analytics measurement ID; omit to skip analytics.

Email sign-in redirect

The storefront uses Firebase passwordless (email-link) sign-in. After a user clicks the link in their inbox, Firebase redirects them back to this absolute URL to complete authentication.

VariableRequiredExampleDescription
NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URLYeshttp://localhost:3000/complete-loginAbsolute URL the email sign-in link returns to. Consumed by src/lib/firebase/authService.ts.

The host in this URL must be listed under Firebase Auth → Settings → Authorized domains, or the redirect is rejected. In production, point it at your deployed domain's /complete-login route.

Identity provider (optional)

Firebase Auth answers "who is calling?" unless AUTH_PROVIDER says otherwise. authjs puts sessions in your SQL database and drops the Firebase project entirely — authentication covers what that changes, and why it only works on a SQL backend.

VariableRequiredDefaultDescription
AUTH_PROVIDEROptional(empty → firebase)firebase or authjs. An unrecognized value throws rather than letting requests through unauthenticated.
NEXT_PUBLIC_AUTH_PROVIDEROptional(empty → firebase)The same name again, for the browser — it decides whether a Firebase ID token is attached to /api/commerce calls. Must match AUTH_PROVIDER.
AUTH_SECRETWith authjs—Session-signing secret. Generate one with openssl rand -base64 32.
AUTH_GOOGLE_IDOptional—Google OAuth client ID, callback /api/auth/callback/google.
AUTH_GOOGLE_SECRETOptional—Google OAuth client secret.

Magic links reuse RESEND_API_KEY from the section below, so Google is not the only option — but at least one of the two must be configured or the /login screen has no method to offer.

Resend email notifications (optional)

Transactional email is powered by Resend and is entirely optional. When RESEND_API_KEY is empty, the POST /api/quotes/notify and POST /api/notifications/status endpoints skip sending and respond { skipped: true } — the quote flow itself is unaffected.

VariableRequiredDefaultDescription
RESEND_API_KEYOptional(empty → email disabled)Resend API key. Leaving it empty disables all outgoing email.
RESEND_FROM_EMAILOptionalonboarding@resend.devVerified sender address. Falls back to Resend's shared testing sender when unset.
QUOTE_NOTIFICATION_EMAILOptionalsiteConfig.contact.emailInbox that receives new-quote notifications. Falls back to the site contact email.

The fallbacks are applied server-side in src/app/api/quotes/notify/route.ts:

src/app/api/quotes/notify/route.ts
const from = process.env.RESEND_FROM_EMAIL || "onboarding@resend.dev";
const adminRecipient =
  process.env.QUOTE_NOTIFICATION_EMAIL || siteConfig.contact.email;

Rate limiting (optional)

/api/checkout, /api/quotes/notify and /api/notifications/status are rate limited per client IP. Without these variables the counters live in the server process, which on serverless means per instance — set them to move the counters to a shared store so the limit actually holds. Values come from the Upstash console → your database → REST API.

VariableRequiredExampleDescription
UPSTASH_REDIS_REST_URLOptionalhttps://eu1-xxx.upstash.ioUpstash REST endpoint. Both variables must be set, or the limiter stays in-process.
UPSTASH_REDIS_REST_TOKENOptional(from console)REST token. Server-side only — never NEXT_PUBLIC_.

No package is installed for this: the limiter calls the REST API with fetch, so it works in any runtime. If the store is unreachable the request is allowed and [rate-limit] is logged — a limiter outage must not take checkout down.

Seed service account (optional)

npm run seed (scripts/seed.ts) writes the demo catalog through whichever backend is selected, using that backend's own credentials — DATABASE_URL for Postgres/SQLite, SUPABASE_* for Supabase, and Firebase Admin credentials for Firestore. Only the Firestore path needs the variable below: point it at a service-account JSON file from the Firebase console → Project settings → Service accounts → Generate new private key.

VariableRequiredExampleDescription
FIREBASE_SERVICE_ACCOUNTOptional./firebase-service-account.jsonFilesystem path to a service-account JSON key. Also used by firebase-admin for server-side Firestore writes and ID-token verification.

Never commit the service-account file, and never expose it with a NEXT_PUBLIC_ prefix — it grants privileged, RLS-bypassing access to your project. Keep it server-side only.

Stripe Checkout (optional)

Stripe adds a real "Checkout" button alongside the quote flow. The Stripe client is instantiated lazily (src/lib/stripe.ts), so when STRIPE_SECRET_KEY is unset the store stays quote-only and the Checkout button simply does not appear.

src/lib/stripe.ts
export function isStripeEnabled(): boolean {
  return Boolean(process.env.STRIPE_SECRET_KEY);
}
VariableRequiredDescription
STRIPE_SECRET_KEYOptionalStripe secret key. Enables Checkout; leave empty to keep the store quote-only.
STRIPE_WEBHOOK_SECRETOptionalSigning secret used to verify the POST /api/webhooks/stripe endpoint (from stripe listen or the dashboard).
FIREBASE_SERVICE_ACCOUNT_JSONOptionalService-account JSON contents (not a path). Lets the Stripe webhook record orders server-side. Vercel-friendly; alternatively rely on FIREBASE_SERVICE_ACCOUNT.

FIREBASE_SERVICE_ACCOUNT_JSON holds the raw private-key JSON. Treat it as a secret: server-only, never NEXT_PUBLIC_, never committed.

Newsletter storage (optional)

The home-page signup form posts to POST /api/newsletter, which resolves a provider from src/lib/newsletter — the collection no longer accepts writes from the browser. The endpoint's validation and rate limiting are covered in storefront → newsletter signup.

VariableRequiredDefaultDescription
NEWSLETTER_PROVIDEROptional(empty → follows the backend)store writes signups to the database this store already runs; demo accepts and discards them. Unset resolves to demo in demo mode and store on any configured backend. An unrecognized value throws at startup rather than dropping signups quietly.

On Firebase, store needs FIREBASE_SERVICE_ACCOUNT_JSON (or FIREBASE_SERVICE_ACCOUNT): the write happens server-side with the Admin SDK. Without credentials the endpoint accepts the signup, logs a warning, and stores nothing.

Commerce backend selection

The commerce data layer (src/lib/commerce) is backend-agnostic. Firebase is the default and runs entirely client-side. Selecting a SQL backend routes the browser through the /api/commerce RPC route, where a privileged server adapter runs. Firebase Auth stays the default identity provider on every backend — its ID token is verified server-side via firebase-admin, so the FIREBASE_SERVICE_ACCOUNT* credentials are required for the SQL backends too, unless AUTH_PROVIDER=authjs replaces it.

Two variables must agree:

VariableRequiredDefaultDescription
NEXT_PUBLIC_COMMERCE_BACKENDOptional(empty → firebase with a project id, else demo)Which adapter the browser uses.
COMMERCE_BACKENDOptional(empty → firebase)Which server adapter the /api/commerce RPC route resolves.
  • demo: the built-in fixture catalog — no backend at all. Browsing, search, and the cart work; writes (quotes, admin, profile) are rejected with an explanatory error. This is also what an unset value falls back to when NEXT_PUBLIC_FIREBASE_PROJECT_ID is empty. Browser-side only: leave COMMERCE_BACKEND empty, since the server var has no demo case and throws on one.
  • firebase (or unset, with a project id): pure client-side Firebase — no RPC route, no SQL variables needed.
  • sqlite | postgres | supabase: set both variables to the same value and provide the matching credentials below.

For the three SQL backends, NEXT_PUBLIC_COMMERCE_BACKEND and COMMERCE_BACKEND must name the same one. The public var tells the browser which adapter to load; the server one gates the RPC route. A mismatch means the browser talks to a route that refuses its requests.

Admin writes on Firebase

On the Firebase backend, admin mutations are routed through /api/commerce so the server can re-check admin status and record an audit entry the acting admin cannot skip — which needs the FIREBASE_SERVICE_ACCOUNT* credentials above.

VariableRequiredDefaultDescription
NEXT_PUBLIC_ADMIN_WRITESOptional(empty → server-routed)Set to client to write straight from the browser to Firestore, gated by firestore.rules alone and with client-written audit entries. The escape hatch for a deployment with no service-account credentials.

Supabase adapter

Set when COMMERCE_BACKEND=supabase. Values come from Supabase → Project Settings → API. See the Supabase adapter guide.

VariableRequiredExampleDescription
SUPABASE_URLWith Supabasehttps://your-project-ref.supabase.coSupabase project URL.
SUPABASE_SERVICE_ROLE_KEYWith Supabase(from dashboard)Service-role key used by the server adapter.

Never prefix the Supabase variables with NEXT_PUBLIC_. The service-role key bypasses Row Level Security and must never reach the browser.

Prisma adapter — Postgres or SQLite

Set when COMMERCE_BACKEND is postgres or sqlite. Read by both the Prisma CLI (prisma.config.ts) and the runtime adapter (src/lib/commerce/server/prisma/index.ts). See the Postgres/Prisma and SQLite guides.

VariableRequiredExampleDescription
DATABASE_URLWith Postgrespostgresql://user:password@localhost:5432/ecommercePlain PostgreSQL connection string (local, Supabase pooler, Neon, RDS, …).
DATABASE_URLWith SQLitefile:./dev.dbPath to the database file, relative to the project root.

COMMERCE_BACKEND does double duty for Prisma: besides selecting the adapter at runtime it decides which schema file the CLI loads (prisma/schema.prisma vs prisma/schema.sqlite.prisma, which Prisma cannot pick from an env var inside the schema itself). So pass it on the CLI commands, not just in .env.local:

Terminal
COMMERCE_BACKEND=sqlite npm run prisma:generate
COMMERCE_BACKEND=sqlite npm run prisma:push

This project does not use Prisma Postgres / Accelerate — you never need a Prisma account or API key. Set DATABASE_URL to a real postgresql:// URL before running npm run prisma:generate, otherwise the Prisma CLI drops into its hosted-DB onboarding and asks for an API key. Not using Postgres? Leave it blank.

On this page