Skip to content
Last updated Give Feedback

Environment Variables

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)
VariableRDefaultDescription
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_URLRhttp://ecommus.localPublic storefront URL. Used by emails, CORS, and OG tags.
ADMIN_URLRhttp://admin.ecommus.localPublic admin URL. CORS allowlist + email links.
API_URLRhttp://api.ecommus.localPublic API URL. CORS + storefront SSR fetches.
SUPER_ADMIN_URLSuper-admin URL (Enterprise + MediaDesign-internal).
LOG_LEVELinfotrace/debug/info/warn/error. Pino structured logs.
TZEurope/BucharestServer timezone (date math, cron, fiscal reports).
VariableRDefaultDescription
DATABASE_MODERpglitepglite (dev only) or postgres. Production fail-fast if not postgres.
DATABASE_URLRPostgreSQL 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_DBUsed by Docker Compose to bootstrap the Postgres container. Code reads DATABASE_URL.
VariableRDefaultDescription
JWT_ACCESS_SECRETRdev-access-secret32+ chars, random. Boot fails if dev-* in production.
JWT_REFRESH_SECRETRdev-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:

Terminal window
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
# or
openssl rand -hex 32
VariableRDefaultDescription
ECOMMUS_LICENSE_JWTRThe 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.

License server (MediaDesign-internal only)

Section titled “License server (MediaDesign-internal only)”

These keys are used by apps/license-server. Customer installs leave them empty.

VariableRDefaultDescription
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_TOKENRBearer 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.
VariableRDefaultDescription
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.
VariableRDefaultDescription
REDIS_URLRredis://redis:6379BullMQ + cache. In dev, an in-memory fallback works; production needs a real Redis.
VariableRDefaultDescription
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_PASSFallback SMTP. Used only when RESEND_API_KEY is empty.
SMTP_FROMnoreply@ecommus.localSMTP From: header.
VariableRDefaultDescription
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_ENDPOINTSet only for non-AWS S3-compatible providers (Wasabi, Backblaze B2, Cloudflare R2).
ASSET_CDN_URLPublic CDN base for served assets (CloudFront, Cloudflare). Replaces direct bucket URLs.
VariableRDefaultDescription
PAYMENT_DRIVERRmockstripe / 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.

ANAF e-Factura RO (mandatory for every RO ecommerce customer)

Section titled “ANAF e-Factura RO (mandatory for every RO ecommerce customer)”

Code in apps/api/src/services/efactura.service.ts requires these. Without ANAF_OAUTH_TOKEN, every order silently fails e-Factura submission.

VariableRDefaultDescription
ANAF_CIFRFirmă’s CIF/CUI without the RO prefix.
ANAF_OAUTH_TOKENRBearer token from ANAF SPV.
ANAF_CLIENT_IDOAuth client_id (used by the token-refresh flow).
ANAF_CLIENT_SECRETOAuth client_secret.
ANAF_MODERtesttest / 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.

VariableRDescription
SAMEDAY_USERNAME / SAMEDAY_PASSWORDR*Sameday Courier API credentials.
FAN_USERNAME / FAN_PASSWORD / FAN_CLIENT_IDR*FAN Courier API.
DPD_USERNAME / DPD_PASSWORDR*DPD RO.
VariableRDescription
SMSLINK_USERNAMER*smslink API account.
SMSLINK_PASSWORDR*smslink API password.
SMSLINK_FROMR*Display sender (≤ 11 chars).
VariableRDefaultDescription
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.
VariableDefaultDescription
RATE_LIMIT_MAX_REQUESTS200Per-IP per-window cap on the Fastify rate limiter.
RATE_LIMIT_WINDOW_MS60000Window size in ms.
VariableRDefaultDescription
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).

VariableRDescription
ECOMMUS_DATA_KEYRMaster KEK that wraps per-row DEKs. openssl rand -hex 32. Rotating re-keys every encrypted row (see ADR-028).
VariableRDescription
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_TOKENRBearer for /metrics (Prometheus scrape). Must be set; the endpoint is auth-gated.

Analytics / pixels (optional, per-tenant configurable in admin too)

Section titled “Analytics / pixels (optional, per-tenant configurable in admin too)”
VariableDescription
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.

AI features (optional, all behind feature flags)

Section titled “AI features (optional, all behind feature flags)”
VariableDescription
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:

VariableDefaultDescription
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).

Quick reference — minimum required for production

Section titled “Quick reference — minimum required for production”

To pass validateProductionConfig():

Terminal window
NODE_ENV=production
DATABASE_MODE=postgres
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).