Skip to content

Environment reference

Configuration is read once from .env (or the file ENV_FILE names) and from the process environment, then validated. An invalid value fails at boot with the variable named, rather than as a 500 later. .env is gitignored; use platform environment variables in production.

Variable Default Notes
NODE_ENV development production turns on the setup token, the compiled-client handler, Secure cookies and TRUST_PROXY=1.
API_PORT 4000 Separate from Vite’s port. Avoid 5000 on macOS: AirPlay Receiver binds it.
API_HOST 0.0.0.0 Set 127.0.0.1 on a VM with a reverse proxy on the same box.
TRUST_PROXY 1 in production, false otherwise false, true, a hop count, or a list of addresses or subnets. The login rate limit and the session cookie depend on it.
PUBLIC_URL http://localhost:5173 Where shoppers reach the store. Behind Stripe’s success and cancel URLs, the sitemap, canonical and Open Graph tags, and every emailed link. A deploy that leaves the default fails silently.
SESSION_SECRET generated per boot in development Required in production, at least 32 characters. openssl rand -base64 32. Without it in development, restarting the API signs you out.
SETUP_TOKEN printed at boot in production At least 16 characters. What the first-run wizard asks for before it will create the first administrator.
DATABASE_URL file:./data/beluga.sqlite file: for SQLite, postgres:// for Postgres.
Variable Notes
STRIPE_SECRET_KEY sk_test_… or sk_live_…. Server-only. A store without it browses but cannot take money.
STRIPE_PUBLISHABLE_KEY pk_…. Public by design; served to the browser through /api/store.
STRIPE_WEBHOOK_SECRET whsec_… from the Stripe dashboard endpoint, or from stripe listen. Without it no order is ever marked paid.
Variable Default Notes
ASSETS_DIR public/assets Where uploads are written and served from under the local driver. Relative to the working directory. Point it at a volume. The URL is always /assets/<path> regardless.
MAX_UPLOAD_BYTES 20 MB Ceiling for imagery; uploads are re-encoded and capped at 2400 px anyway.
ASSETS_S3_BUCKET unset Setting it selects the bucket driver and makes the next four required.
ASSETS_S3_REGION auto for R2 and similar.
ASSETS_S3_ACCESS_KEY_ID
ASSETS_S3_SECRET_ACCESS_KEY
ASSETS_PUBLIC_URL The bucket’s public address or a CDN in front of it. /assets/<path> redirects here.
ASSETS_S3_ENDPOINT AWS For any provider other than AWS itself: Spaces, R2, Backblaze, MinIO.
ASSETS_S3_ACL unset public-read for providers that want it per object (Spaces does; AWS with bucket-owner-enforced ownership rejects it).

A half-configured bucket group is refused at boot with the missing variables named, and a stray ASSETS_S3_* value with no bucket is refused too, because it would quietly write to disk on a platform that loses it. The boot log says which driver is active next to the port.

Variable Notes
SMTP_URL Any SMTP provider: smtps://user:pass@smtp.example.com:465. Until set, sending is a logged no-op, not an error.
EMAIL_FROM "My Store <orders@example.com>".
Variable Default Notes
WEBHOOK_ALLOW_INSECURE_TARGETS false Lets outbound endpoints be plain http:// or resolve to private addresses. Leave it off anywhere public.
Variable Default Notes
VITE_BELUGA_API true false renders the bundled demo fixture with no database, for UI work. Confusing if you set it and forget.

npm run db:seed reads ADMIN_EMAIL and ADMIN_PASSWORD from the environment and creates the administrator if none exists, which is how containers and CI get a store without answering prompts.

playwright.config.ts points ENV_FILE at a file that does not exist, on purpose, so .env is irrelevant to npm run test:e2e. Copying one in will not fix a failing run.