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:
cp .env.example .env.localNothing 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.
| Variable | Required | Feature |
|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | Yes | Firebase / Auth / Firestore |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | Firebase Auth |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | Firebase project |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | Yes | Firebase Storage |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | Yes | Firebase |
NEXT_PUBLIC_FIREBASE_APP_ID | Yes | Firebase |
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID | Optional | Firebase Analytics |
NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URL | Yes | Passwordless email sign-in |
RESEND_API_KEY | Optional | Quote/status email notifications |
RESEND_FROM_EMAIL | Optional | Email sender address |
QUOTE_NOTIFICATION_EMAIL | Optional | New-quote notification inbox |
FIREBASE_SERVICE_ACCOUNT | Optional | npm run seed, server-side Firestore |
STRIPE_SECRET_KEY | Optional | Stripe Checkout |
STRIPE_WEBHOOK_SECRET | Optional | Stripe webhook verification |
FIREBASE_SERVICE_ACCOUNT_JSON | Optional | Webhook / server-side Firestore writes |
UPSTASH_REDIS_REST_URL | Optional | Shared rate-limit store |
UPSTASH_REDIS_REST_TOKEN | Optional | Shared rate-limit store |
AUTH_PROVIDER | Optional | Server-side identity provider (default firebase) |
NEXT_PUBLIC_AUTH_PROVIDER | Optional | Browser-side identity provider (must match) |
AUTH_SECRET | With Auth.js | Auth.js session-signing secret |
AUTH_GOOGLE_ID | Optional | Auth.js Google sign-in |
AUTH_GOOGLE_SECRET | Optional | Auth.js Google sign-in |
NEWSLETTER_PROVIDER | Optional | Where newsletter signups are stored |
NEXT_PUBLIC_COMMERCE_BACKEND | Optional | Browser commerce adapter (demo, firebase, sqlite, postgres, supabase) |
COMMERCE_BACKEND | Optional | Server commerce adapter |
NEXT_PUBLIC_ADMIN_WRITES | Optional | Opt out of server-routed admin writes (Firebase) |
SUPABASE_URL | Optional | Supabase backend |
SUPABASE_SERVICE_ROLE_KEY | Optional | Supabase backend |
DATABASE_URL | Optional | Postgres/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.
| Variable | Required | Example | Description |
|---|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | Yes | (from console) | Web API key for the Firebase JS SDK. |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | your-project.firebaseapp.com | Auth domain used by the sign-in flow. |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | your-project | Firebase / Google Cloud project ID. |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | Yes | your-project.appspot.com | Storage bucket for uploaded assets. |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | Yes | (from console) | Cloud Messaging sender ID. |
NEXT_PUBLIC_FIREBASE_APP_ID | Yes | (from console) | Firebase web app ID. |
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID | Optional | (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.
| Variable | Required | Example | Description |
|---|---|---|---|
NEXT_PUBLIC_EMAIL_SIGNIN_REDIRECT_URL | Yes | http://localhost:3000/complete-login | Absolute 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.
| Variable | Required | Default | Description |
|---|---|---|---|
AUTH_PROVIDER | Optional | (empty → firebase) | firebase or authjs. An unrecognized value throws rather than letting requests through unauthenticated. |
NEXT_PUBLIC_AUTH_PROVIDER | Optional | (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_SECRET | With authjs | — | Session-signing secret. Generate one with openssl rand -base64 32. |
AUTH_GOOGLE_ID | Optional | — | Google OAuth client ID, callback /api/auth/callback/google. |
AUTH_GOOGLE_SECRET | Optional | — | 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.
| Variable | Required | Default | Description |
|---|---|---|---|
RESEND_API_KEY | Optional | (empty → email disabled) | Resend API key. Leaving it empty disables all outgoing email. |
RESEND_FROM_EMAIL | Optional | onboarding@resend.dev | Verified sender address. Falls back to Resend's shared testing sender when unset. |
QUOTE_NOTIFICATION_EMAIL | Optional | siteConfig.contact.email | Inbox 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:
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.
| Variable | Required | Example | Description |
|---|---|---|---|
UPSTASH_REDIS_REST_URL | Optional | https://eu1-xxx.upstash.io | Upstash REST endpoint. Both variables must be set, or the limiter stays in-process. |
UPSTASH_REDIS_REST_TOKEN | Optional | (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.
| Variable | Required | Example | Description |
|---|---|---|---|
FIREBASE_SERVICE_ACCOUNT | Optional | ./firebase-service-account.json | Filesystem 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.
export function isStripeEnabled(): boolean {
return Boolean(process.env.STRIPE_SECRET_KEY);
}| Variable | Required | Description |
|---|---|---|
STRIPE_SECRET_KEY | Optional | Stripe secret key. Enables Checkout; leave empty to keep the store quote-only. |
STRIPE_WEBHOOK_SECRET | Optional | Signing secret used to verify the POST /api/webhooks/stripe endpoint (from stripe listen or the dashboard). |
FIREBASE_SERVICE_ACCOUNT_JSON | Optional | Service-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.
| Variable | Required | Default | Description |
|---|---|---|---|
NEWSLETTER_PROVIDER | Optional | (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:
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_COMMERCE_BACKEND | Optional | (empty → firebase with a project id, else demo) | Which adapter the browser uses. |
COMMERCE_BACKEND | Optional | (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 whenNEXT_PUBLIC_FIREBASE_PROJECT_IDis empty. Browser-side only: leaveCOMMERCE_BACKENDempty, since the server var has nodemocase 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.
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_ADMIN_WRITES | Optional | (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.
| Variable | Required | Example | Description |
|---|---|---|---|
SUPABASE_URL | With Supabase | https://your-project-ref.supabase.co | Supabase project URL. |
SUPABASE_SERVICE_ROLE_KEY | With 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.
| Variable | Required | Example | Description |
|---|---|---|---|
DATABASE_URL | With Postgres | postgresql://user:password@localhost:5432/ecommerce | Plain PostgreSQL connection string (local, Supabase pooler, Neon, RDS, …). |
DATABASE_URL | With SQLite | file:./dev.db | Path 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:
COMMERCE_BACKEND=sqlite npm run prisma:generate
COMMERCE_BACKEND=sqlite npm run prisma:pushThis 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.