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.