Source of truth: infra/.env.production.template . Copy that file to /opt/ecommus/.env on the VPS, fill in placeholders, and chmod 600. The API refuses to boot in NODE_ENV=production when any R (“required-in-prod”) variable below is missing or set to a dev-* placeholder — see validateProductionConfig() at apps/api/src/config.ts:48 .
Legend:
R = required in production (boot fails fast if missing)
R* = required only when the matching feature is enabled (ANAF, S3, etc.)
empty = optional (uses default)
Variable R Default Description NODE_ENVdevelopmentdevelopment / test / production. Production triggers validateProductionConfig().APP_NAMEEcommusHuman-readable app name (used in emails, OpenAPI title, etc.) API_PORT4000Fastify HTTP listener port. API_HOST0.0.0.0Bind address. Use 127.0.0.1 if proxying through Caddy/Nginx on same host. APP_URLR http://ecommus.localPublic storefront URL. Used by emails, CORS, and OG tags. ADMIN_URLR http://admin.ecommus.localPublic admin URL. CORS allowlist + email links. API_URLR http://api.ecommus.localPublic API URL. CORS + storefront SSR fetches. SUPER_ADMIN_URL— Super-admin URL (Enterprise + MediaDesign-internal). LOG_LEVELinfotrace/debug/info/warn/error. Pino structured logs.TZEurope/BucharestServer timezone (date math, cron, fiscal reports).
Variable R Default Description DATABASE_MODER pglitepglite (dev only) or postgres. Production fail-fast if not postgres.DATABASE_URLR — PostgreSQL DSN. e.g. postgres://ecommus:pass@host:5432/ecommus_prod DATABASE_DIR./data/dbpglite data directory (dev only). LICENSE_DATABASE_URLR* — Separate DSN for the license-server (only when self-hosting apps/license-server). POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB— Used by Docker Compose to bootstrap the Postgres container. Code reads DATABASE_URL.
Variable R Default Description JWT_ACCESS_SECRETR dev-access-secret32+ chars, random. Boot fails if dev-* in production. JWT_REFRESH_SECRETR dev-refresh-secret32+ chars, random, different from JWT_ACCESS_SECRET . Pair correctness depends on independence. JWT_ACCESS_TTL15mAccess token lifetime (15m, 1h, etc.). JWT_REFRESH_TTL30dRefresh token lifetime. JWT_KEY_IDv1kid claim. Bump when rotating signing keys; old tokens stay valid during the transition (validator accepts multiple kids).
Generate strong secrets:
node -e " console.log(require('crypto').randomBytes(48).toString('hex')) "
Variable R Default Description ECOMMUS_LICENSE_JWTR — The customer license JWT (Ed25519). Pasted from the activation email. See License for the shape. ECOMMUS_LICENSE_SERVER_URLhttps://license.ecommus.roWhere the customer install heartbeats and re-fetches tokens. Override to http://127.0.0.1:4500 for self-hosted server. ECOMMUS_LICENSE_VERIFY_URLhttps://license.ecommus.ro/verifyVerification endpoint (used by the Verdaccio plugin to gate npm install). ECOMMUS_NPM_REGISTRY_URLhttps://npm.ecommus.roPrivate registry for premium plugins/themes.
These keys are used by apps/license-server. Customer installs leave them empty.
Variable R Default Description ECOMMUS_LICENSE_PRIVATE_KEY_FILER /opt/ecommus/keys/license-private.pemEd25519 PEM. Generated by provision.sh. ECOMMUS_LICENSE_PUBLIC_KEY_FILER /opt/ecommus/keys/license-public.pemEd25519 PEM (counterpart). Distributed with each customer copy. LICENSE_SERVER_ADMIN_TOKENR — Bearer for /licenses/issue, /licenses/refund. Boot fails if dev-* or change-me. LICENSE_DEFAULT_PERIOD3moDefault subscription period when issuing a license without an explicit value. STRIPE_LICENSE_WEBHOOK_SECRETR* — Verifies invoice.paid from Stripe. Required when Stripe issues licenses for SaaS / source customers.
Variable R Default Description ECOMMUS_CODE_SIGNING_PUBLIC_KEY_FILER /opt/ecommus/keys/code-signing-public.pemEd25519 PEM. Used to verify plugin/theme bundle signatures. ECOMMUS_CODE_SIGNING_PRIVATE_KEY_FILER* /opt/ecommus/keys/code-signing-private.pemUsed by MediaDesign to sign new bundles before publishing. ECOMMUS_REQUIRE_SIGNED_PLUGINStrue in prodRefuse to load unsigned plugins. Production loader auto-promotes if not set. ECOMMUS_PERMISSION_ENFORCEMENTstrict in prodPlugin permission mode. strict rejects, warn logs but allows. Production auto-promotes.
Variable R Default Description REDIS_URLR redis://redis:6379BullMQ + cache. In dev, an in-memory fallback works; production needs a real Redis.
Variable R Default Description RESEND_API_KEYR* — Preferred email path. Required if you want transactional emails to actually go out. RESEND_FROMEcommus <hello@ecommus.ro>Verified sender address (only set after Resend domain verification completes). SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS— Fallback SMTP. Used only when RESEND_API_KEY is empty. SMTP_FROMnoreply@ecommus.localSMTP From: header.
Variable R Default Description STORAGE_DRIVERlocallocal or s3.STORAGE_LOCAL_DIR/opt/ecommus/uploadsFilesystem path when local. Persistent volume in production. S3_BUCKETR* — Required when STORAGE_DRIVER=s3. AWS_REGIONR* eu-central-1Canonical name (was S3_REGION historically — code reads AWS_REGION via the AWS SDK). AWS_ACCESS_KEY_IDR* — Leave empty when running on EC2 with an IAM role. AWS_SECRET_ACCESS_KEYR* — Same. S3_ENDPOINT— Set only for non-AWS S3-compatible providers (Wasabi, Backblaze B2, Cloudflare R2). ASSET_CDN_URL— Public CDN base for served assets (CloudFront, Cloudflare). Replaces direct bucket URLs.
Variable R Default Description PAYMENT_DRIVERR mockstripe / netopia / euplatesc / mock. Must be explicit in production (Phase A.0 Bug-C2).STRIPE_SECRET_KEYR* — sk_live_... (or sk_test_... while staging).STRIPE_PUBLISHABLE_KEYR* — pk_live_.... Stripe canonical name; legacy STRIPE_PUBLIC_KEY is no longer read.STRIPE_WEBHOOK_SECRETR* — Verifies storefront-order events. Required when Stripe is the storefront payment driver. NETOPIA_POS_SIGNATURER* — Netopia merchant signature. NETOPIA_MODEsandboxsandbox / live.EUPLATESC_MIDR* — EuPlatesc merchant ID.
Code in apps/api/src/services/efactura.service.ts requires these. Without ANAF_OAUTH_TOKEN, every order silently fails e-Factura submission.
Variable R Default Description ANAF_CIFR — Firmă’s CIF/CUI without the RO prefix. ANAF_OAUTH_TOKENR — Bearer token from ANAF SPV. ANAF_CLIENT_ID— OAuth client_id (used by the token-refresh flow). ANAF_CLIENT_SECRET— OAuth client_secret. ANAF_MODER testtest / prod. Stay on test until KYC complete and ready to submit live e-Facturas.
Required for fulfilment if the customer ships physical goods. Paste from each carrier’s portal.
Variable R Description SAMEDAY_USERNAME / SAMEDAY_PASSWORDR* Sameday Courier API credentials. FAN_USERNAME / FAN_PASSWORD / FAN_CLIENT_IDR* FAN Courier API. DPD_USERNAME / DPD_PASSWORDR* DPD RO.
Variable R Description SMSLINK_USERNAMER* smslink API account. SMSLINK_PASSWORDR* smslink API password. SMSLINK_FROMR* Display sender (≤ 11 chars).
Variable R Default Description TENANT_MODEmultisingle (one row in tenants table, auto-resolve) or multi (subdomain or header).AUTO_SETUPfalseNever auto-create demo data in production. Dev-only convenience. ECOMMUS_WORKERStrueEnables background workers (low-stock alerter, reservation sweeper, license heartbeat, expiry warnings, etc.). Disable for split deploys.
Variable Default Description RATE_LIMIT_MAX_REQUESTS200Per-IP per-window cap on the Fastify rate limiter. RATE_LIMIT_WINDOW_MS60000Window size in ms.
Variable R Default Description DEFAULT_CURRENCYRON (prod template) / EUR (code default)RO market default; override per-tenant via admin UI. DEFAULT_LOCALEroro / en / hu / bg. Per-tenant override available.
Phase 0 §1.6 — column-level envelope encryption for payment_methods.config and settings.value (ANAF tokens).
Variable R Description ECOMMUS_DATA_KEYR Master KEK that wraps per-row DEKs. openssl rand -hex 32. Rotating re-keys every encrypted row (see ADR-028).
Variable R Description SENTRY_DSNSentry SaaS or self-hosted GlitchTip (recommended — €0, EU-hosted, GDPR-friendly, Sentry-DSN-compatible). Leave empty until stack lands. OTEL_EXPORTER_OTLP_ENDPOINTOpenTelemetry exporter (Tempo / Jaeger). No-op when unset. METRICS_TOKENR Bearer for /metrics (Prometheus scrape). Must be set; the endpoint is auth-gated.
Variable Description GA4_MEASUREMENT_IDG-XXXXXXXXXX — GA4 client-side.GA4_API_SECRETServer-side Measurement Protocol (Phase A.0 Bug-S4). META_PIXEL_ID / META_CAPI_TOKENMeta Conversions API. TIKTOK_PIXEL_ID / TIKTOK_ACCESS_TOKENTikTok pixel + Events API. TAWKTO_WIDGET_IDTawk.to live chat widget.
Variable Description ANTHROPIC_API_KEYClaude API key — preferred for product description / SEO copy generation. OPENAI_API_KEYOpenAI alternative.
These are read by apps/admin and apps/storefront-astro at build / SSR time:
Variable Default Description NEXT_PUBLIC_API_URL/apiAPI base URL exposed to the browser (proxied via Next.js). API_URLhttp://localhost:4000API URL used by storefront SSR (server-to-server, never reaches the browser).
To pass validateProductionConfig():
DATABASE_URL = postgres://...
JWT_ACCESS_SECRET = <32+ random chars>
JWT_REFRESH_SECRET = <different 32+ random chars>
# If self-hosting license server, also:
LICENSE_SERVER_ADMIN_TOKEN = <32+ random chars> # not "dev-*" / "change-me"
Plus, depending on what features the customer enables: payment driver creds (STRIPE_* / NETOPIA_*), email (RESEND_API_KEY), ANAF (ANAF_*), storage (STORAGE_DRIVER + S3_*), shipping (SAMEDAY_* etc.), SMS (SMSLINK_*), encryption (ECOMMUS_DATA_KEY), metrics (METRICS_TOKEN).