nest-api environment
How configuration works
Section titled “How configuration works”Every variable is validated with Zod when the app boots
(src/common/services/config.service.ts), and the process refuses to start
if one is missing or malformed, instead of failing later inside a request.
- An empty value counts as missing for a required string:
SMTP_USER=''fails. - Numbers must parse:
PORT=''fails (a default applies only when the variable is absent altogether). - App code never reads
process.env: it injectsCustomConfigService. Adding a variable means three edits in that file (the interface, the schema andgetVariables) and a line in.env.example.
Development and production values
Section titled “Development and production values”| Variable | What to set |
|---|---|
APP_ENV | Production: deployment.environment.name. In nest-api, development also adds local servers to the OpenAPI document. |
APP_NAME | Production: Your product's display name. Used in emails, the Swagger title and better-auth. Set by--name. |
APP_HOST | Production: 127.0.0.1 would make it unreachable from outside the container. |
PORT | Production: The port the process listens on (nest-api: the container's exposed port). Changing it after generation means changingSERVER_HOST and BETTER_AUTH_URL too; --port at generation does all three. |
SERVER_HOST | Production: The API's public URL, e.g. /health HTTP check, which calls it from inside the container, so it must resolve there. |
FRONTEND_HOST | Production: The front end's origin, e.g. MISC_CORS_ORIGINS. |
MISC_CORS_ORIGINS | Extra origins beyond FRONTEND_HOST, comma separated, trusted by auth and allowed by CORS: every react-monorepo app goes here or in FRONTEND_HOST. A full origin (`http://localhost:3001`) is used as written; a bare hostname (`example.com`) expands to https://example.com and its auth., app. and admin. subdomains. Production: Every other front-end origin, comma-separated; empty for a single front end. A full origin (http://localhost:3001) is used as written; a bare hostname (example.com) expands to https origins for it and its auth., app. and admin. subdomains. See Auth, cookies and CORS. |
DATABASE_URL | Production: Where to get it: Your PostgreSQL provider. A worker's value must be the API's database. URL-encode special characters in the password. See PostgreSQL. |
BETTER_AUTH_SECRET | openssl rand -base64 32 Production: A random 32-byte value: |
BETTER_AUTH_URL | openssl rand -base64 32 Production: The API's public URL, like <this>/auth/callback/<provider>. |
BETTER_AUTH_COOKIE_DOMAIN | openssl rand -base64 32 Empty: the session cookie belongs to the API's own host, which is what localhost development and a single-origin frontend need. Set a parent domain with a leading dot (`.example.com`) when frontends live on subdomains, to share one login across them; locally, `.lvh.me` does the same with `*.lvh.me` hosts (every subdomain resolves to 127.0.0.1). The domain must cover the host the browser talks to, or the cookie is dropped and every sign-in bounces. Production: Empty for a single front-end origin; |
BETTER_AUTH_COOKIE_PREFIX | openssl rand -base64 32 Empty: the session cookie belongs to the API's own host, which is what localhost development and a single-origin frontend need. Set a parent domain with a leading dot (`.example.com`) when frontends live on subdomains, to share one login across them; locally, `.lvh.me` does the same with `*.lvh.me` hosts (every subdomain resolves to 127.0.0.1). The domain must cover the host the browser talks to, or the cookie is dropped and every sign-in bounces. Production: Keep the generated value (the project name). Distinguishes this project's cookies from others on the same parent domain. The runtime and tooling instances must agree. |
SUPERUSER_EMAIL | Seeded by `yarn db:seed`. Production: The first admin's address. yarn db:seed creates this user, verified, with the superuser role. Seeding again fails on the existing email. |
SUPERUSER_PASSWORD | Seeded by `yarn db:seed`. Production: A strong password, changed after first sign-in. Only read by the seeder. |
OAUTH_GOOGLE_CLIENT_ID | Production: Your Google OAuth client id. Where to get it: Google Cloud console → Credentials. See Google sign-in. Empty disables Google sign-in (the button fails). |
OAUTH_GOOGLE_CLIENT_SECRET | Production: The client's secret. Where to get it: Google Cloud console → Credentials. |
LOG_LEVEL | Production: trace, debug, info, warn, error, fatal, silent. An empty value fails validation in nest-api. |
OPENOBSERVE_URL | OpenTelemetry → OpenObserve: traces, logs and metrics. Leave empty and telemetry stays off (local development, tests). Per project: OPENOBSERVE_ORG the project's organization identifier OPENOBSERVE_TOKEN that org's ingestion token (IAM → Ingestion Tokens, o2oi_…) OPENOBSERVE_STREAM defaults to the service name (OTEL_SERVICE_NAME). Any other OTLP backend: set OTEL_EXPORTER_OTLP_ENDPOINT/HEADERS instead. Production: Your OpenObserve instance. Telemetry is on only when the URL, org and token are all set. See OpenObserve. |
OPENOBSERVE_ORG | OpenTelemetry → OpenObserve: traces, logs and metrics. Leave empty and telemetry stays off (local development, tests). Per project: OPENOBSERVE_ORG the project's organization identifier OPENOBSERVE_TOKEN that org's ingestion token (IAM → Ingestion Tokens, o2oi_…) OPENOBSERVE_STREAM defaults to the service name (OTEL_SERVICE_NAME). Any other OTLP backend: set OTEL_EXPORTER_OTLP_ENDPOINT/HEADERS instead. Production: The project's organization identifier. Where to get it: OpenObserve → organization settings (or the org segment of its URLs). |
OPENOBSERVE_TOKEN | OpenTelemetry → OpenObserve: traces, logs and metrics. Leave empty and telemetry stays off (local development, tests). Per project: OPENOBSERVE_ORG the project's organization identifier OPENOBSERVE_TOKEN that org's ingestion token (IAM → Ingestion Tokens, o2oi_…) OPENOBSERVE_STREAM defaults to the service name (OTEL_SERVICE_NAME). Any other OTLP backend: set OTEL_EXPORTER_OTLP_ENDPOINT/HEADERS instead. Production: The organization's ingestion token ( Where to get it: OpenObserve → IAM → Ingestion Tokens. Write-only, but secret: never use it in a browser app (front ends use a RUM token). |
OTEL_SERVICE_NAME | OpenTelemetry → OpenObserve: traces, logs and metrics. Leave empty and telemetry stays off (local development, tests). Per project: OPENOBSERVE_ORG the project's organization identifier OPENOBSERVE_TOKEN that org's ingestion token (IAM → Ingestion Tokens, o2oi_…) OPENOBSERVE_STREAM defaults to the service name (OTEL_SERVICE_NAME). Any other OTLP backend: set OTEL_EXPORTER_OTLP_ENDPOINT/HEADERS instead. Production: A distinct name per service: |
HEALTH_HEAP_MAX_MB | Limits past which /health reports down. Size them to the container's real limits: an orchestrator restarts instances that fail this check. Production: Below the container's memory limit, e.g. 75% of it. Past it,/health reports down and orchestrators restart the instance. |
HEALTH_RSS_MAX_MB | Limits past which /health reports down. Size them to the container's real limits: an orchestrator restarts instances that fail this check. Production: Below the container's memory limit. Resident memory, heap included. |
HEALTH_DISK_THRESHOLD | Limits past which /health reports down. Size them to the container's real limits: an orchestrator restarts instances that fail this check. Fraction (0–1) of HEALTH_DISK_PATH's volume that may be used. Production: The fraction of the volume that may be used, e.g. |
HEALTH_DISK_PATH | Limits past which /health reports down. Size them to the container's real limits: an orchestrator restarts instances that fail this check. Fraction (0–1) of HEALTH_DISK_PATH's volume that may be used. Production: The volume to watch: |
BASIC_AUTH_USER | Guards /docs, /openapi-json and /dashboard. Production: A new operator username. Guards/docs, /openapi, /openapi-json and /dashboard. Front ends' yarn gen uses it as API_DOC_USER. |
BASIC_AUTH_PASS | Guards /docs, /openapi-json and /dashboard. Production: A strong password, not the example's. |
REDIS_HOST | Production: Your Redis host; identical on the API and every worker. A worker on a different Redis connects fine and never receives a job. See Redis. |
REDIS_PORT | Production: Your Redis port (6379). |
REDIS_USER | Production: An ACL user, if your Redis uses them. |
REDIS_PASSWORD | Production: The Redis password. |
SMTP_HOST | Production: Your mail provider's SMTP host. See Email (SMTP), including a local catcher. |
SMTP_PORT | Production: |
SMTP_SECURE | Production: |
SMTP_USER | Production: The SMTP username. Where to get it: Your mail provider. Required and empty by default: the API won't start until it's set. |
SMTP_PASSWORD | Production: The SMTP password or API key. Where to get it: Your mail provider. Required and empty by default. |
SMTP_FROM | Production: |
SMTP_SUPPORT_EMAIL | Production: Your support address. Shown in the welcome email; keep it equal to a landing site'sVITE_APP_SUPPORT_EMAIL. |
S3_REGION | Production: The bucket's region. |
S3_ENDPOINT | Leave empty for AWS; set it for MinIO or another S3-compatible host. Production: The endpoint URL, even on AWS: |
S3_BUCKET_NAME | Leave empty for AWS; set it for MinIO or another S3-compatible host. Production: The bucket. Must allow object ACLs: uploads outside the private folders arepublic-read. |
S3_ACCESS_KEY_ID | Leave empty for AWS; set it for MinIO or another S3-compatible host. Production: An access key for the bucket. Where to get it: Your storage provider's IAM. |
S3_SECRET_ACCESS_KEY | Leave empty for AWS; set it for MinIO or another S3-compatible host. Production: Its secret. |
TEMPORAL_ADDRESS | Temporal: durable workflows. Locally, the dev server in compose.yml (no TLS; UI on http://localhost:8233). In production, the server address, the project's namespace and its mTLS client certificate (plus the CA that signed the server's), as PEM. Escaped \n are accepted, so each fits on one line. Production: Your Temporal frontend, e.g. |
TEMPORAL_NAMESPACE | Temporal: durable workflows. Locally, the dev server in compose.yml (no TLS; UI on http://localhost:8233). In production, the server address, the project's namespace and its mTLS client certificate (plus the CA that signed the server's), as PEM. Escaped \n are accepted, so each fits on one line. Production: The project's namespace, the same on the API and its workers. |
TEMPORAL_TASK_QUEUE | Temporal: durable workflows. Locally, the dev server in compose.yml (no TLS; UI on http://localhost:8233). In production, the server address, the project's namespace and its mTLS client certificate (plus the CA that signed the server's), as PEM. Escaped \n are accepted, so each fits on one line. Where workflows started here are queued. The worker that runs them must poll the same queue (its TEMPORAL_TASK_QUEUE); a mismatch is silent, workflows just sit waiting. Production: The queue a worker polls. API: where workflows are started. Worker: the queue it polls. A mismatch is silent: workflows wait forever. |
TEMPORAL_TLS_CA | Temporal: durable workflows. Locally, the dev server in compose.yml (no TLS; UI on http://localhost:8233). In production, the server address, the project's namespace and its mTLS client certificate (plus the CA that signed the server's), as PEM. Escaped \n are accepted, so each fits on one line. Where workflows started here are queued. The worker that runs them must poll the same queue (its TEMPORAL_TASK_QUEUE); a mismatch is silent, workflows just sit waiting. Production: The CA that signed the server's certificate, as PEM; empty if it's publicly trusted. Escaped\n are accepted, so it fits on one line. |
TEMPORAL_TLS_CERT | Temporal: durable workflows. Locally, the dev server in compose.yml (no TLS; UI on http://localhost:8233). In production, the server address, the project's namespace and its mTLS client certificate (plus the CA that signed the server's), as PEM. Escaped \n are accepted, so each fits on one line. Where workflows started here are queued. The worker that runs them must poll the same queue (its TEMPORAL_TASK_QUEUE); a mismatch is silent, workflows just sit waiting. Production: The project's client certificate, as PEM. Where to get it: Issued by whoever runs your Temporal (the gateway's CA). Set it together withTEMPORAL_TLS_KEY, or startup fails. |
TEMPORAL_TLS_KEY | Temporal: durable workflows. Locally, the dev server in compose.yml (no TLS; UI on http://localhost:8233). In production, the server address, the project's namespace and its mTLS client certificate (plus the CA that signed the server's), as PEM. Escaped \n are accepted, so each fits on one line. Where workflows started here are queued. The worker that runs them must poll the same queue (its TEMPORAL_TASK_QUEUE); a mismatch is silent, workflows just sit waiting. Production: The client certificate's private key, as PEM. A secret: keep it in your secret store. |
RATE_LIMIT_ENABLED | Production: .env.test so specs don't share a rate-limit window. |
RATE_LIMIT_TTL | Throttler TTLs are in milliseconds. Production: The window for app routes, in milliseconds (default 60000). |
RATE_LIMIT_MAX | Throttler TTLs are in milliseconds. Production: Requests per window per client IP on app routes (default 120). |
RATE_LIMIT_AUTH_WINDOW | Throttler TTLs are in milliseconds. better-auth's own window is in seconds. Production: better-auth's window for |
RATE_LIMIT_AUTH_MAX | Throttler TTLs are in milliseconds. better-auth's own window is in seconds. Production: Requests per auth window per client IP (default 100). |
RATE_LIMIT_IP_HEADERS | Throttler TTLs are in milliseconds. better-auth's own window is in seconds. Load-bearing behind a proxy: if no IP can be resolved, better-auth skips rate limiting entirely and only warns. Production: The header your reverse proxy sets to the client's IP ( |
GROWTHBOOK_API_HOST | Production: feature-flags and empty by default. See GrowthBook. |
GROWTHBOOK_CLIENT_KEY | Production: An SDK connection's client key ( Where to get it: GrowthBook → SDK Connections. |
Test values
Section titled “Test values”.env.test (from .env.test.example) is what every test script runs against:
each is wrapped in dotenv -e .env.test --. It points at the separate test
stack (PostgreSQL 5499, Redis 6399), never at your development database: the
e2e factory truncates every table between specs. Variables it doesn’t set
(telemetry, health, Temporal) fall back to schema defaults or to .env.
| Variable | What to set |
|---|---|
APP_ENV | Production: deployment.environment.name. In nest-api, development also adds local servers to the OpenAPI document. |
APP_NAME | Production: Your product's display name. Used in emails, the Swagger title and better-auth. Set by--name. |
APP_HOST | Production: 127.0.0.1 would make it unreachable from outside the container. |
PORT | Production: The port the process listens on (nest-api: the container's exposed port). Changing it after generation means changingSERVER_HOST and BETTER_AUTH_URL too; --port at generation does all three. |
SERVER_HOST | Production: The API's public URL, e.g. /health HTTP check, which calls it from inside the container, so it must resolve there. |
FRONTEND_HOST | Production: The front end's origin, e.g. MISC_CORS_ORIGINS. |
MISC_CORS_ORIGINS | Production: Every other front-end origin, comma-separated; empty for a single front end. A full origin (http://localhost:3001) is used as written; a bare hostname (example.com) expands to https origins for it and its auth., app. and admin. subdomains. See Auth, cookies and CORS. |
DATABASE_URL | Production: Where to get it: Your PostgreSQL provider. A worker's value must be the API's database. URL-encode special characters in the password. See PostgreSQL. |
BETTER_AUTH_SECRET | Production: A random 32-byte value: |
BETTER_AUTH_URL | Production: The API's public URL, like <this>/auth/callback/<provider>. |
BETTER_AUTH_COOKIE_DOMAIN | Production: Empty for a single front-end origin; |
BETTER_AUTH_COOKIE_PREFIX | Production: Keep the generated value (the project name). Distinguishes this project's cookies from others on the same parent domain. The runtime and tooling instances must agree. |
SUPERUSER_EMAIL | Production: The first admin's address. yarn db:seed creates this user, verified, with the superuser role. Seeding again fails on the existing email. |
SUPERUSER_PASSWORD | Production: A strong password, changed after first sign-in. Only read by the seeder. |
OAUTH_GOOGLE_CLIENT_ID | Production: Your Google OAuth client id. Where to get it: Google Cloud console → Credentials. See Google sign-in. Empty disables Google sign-in (the button fails). |
OAUTH_GOOGLE_CLIENT_SECRET | Production: The client's secret. Where to get it: Google Cloud console → Credentials. |
LOG_LEVEL | Production: trace, debug, info, warn, error, fatal, silent. An empty value fails validation in nest-api. |
BASIC_AUTH_USER | Production: A new operator username. Guards/docs, /openapi, /openapi-json and /dashboard. Front ends' yarn gen uses it as API_DOC_USER. |
BASIC_AUTH_PASS | Production: A strong password, not the example's. |
REDIS_HOST | Production: Your Redis host; identical on the API and every worker. A worker on a different Redis connects fine and never receives a job. See Redis. |
REDIS_PORT | Production: Your Redis port (6379). |
REDIS_USER | Production: An ACL user, if your Redis uses them. |
REDIS_PASSWORD | Production: The Redis password. |
SMTP_HOST | Never delivered — AppFactory replaces MailService with a no-op. Production: Your mail provider's SMTP host. See Email (SMTP), including a local catcher. |
SMTP_PORT | Never delivered — AppFactory replaces MailService with a no-op. Production: |
SMTP_SECURE | Never delivered — AppFactory replaces MailService with a no-op. Production: |
SMTP_USER | Never delivered — AppFactory replaces MailService with a no-op. Production: The SMTP username. Where to get it: Your mail provider. Required and empty by default: the API won't start until it's set. |
SMTP_PASSWORD | Never delivered — AppFactory replaces MailService with a no-op. Production: The SMTP password or API key. Where to get it: Your mail provider. Required and empty by default. |
SMTP_FROM | Never delivered — AppFactory replaces MailService with a no-op. Production: |
SMTP_SUPPORT_EMAIL | Never delivered — AppFactory replaces MailService with a no-op. Production: Your support address. Shown in the welcome email; keep it equal to a landing site'sVITE_APP_SUPPORT_EMAIL. |
S3_REGION | Production: The bucket's region. |
S3_ENDPOINT | Production: The endpoint URL, even on AWS: |
S3_BUCKET_NAME | Production: The bucket. Must allow object ACLs: uploads outside the private folders arepublic-read. |
S3_ACCESS_KEY_ID | Production: An access key for the bucket. Where to get it: Your storage provider's IAM. |
S3_SECRET_ACCESS_KEY | Production: Its secret. |
RATE_LIMIT_ENABLED | Off in tests so a shared window cannot leak between specs. Production: .env.test so specs don't share a rate-limit window. |
RATE_LIMIT_TTL | Off in tests so a shared window cannot leak between specs. Production: The window for app routes, in milliseconds (default 60000). |
RATE_LIMIT_MAX | Off in tests so a shared window cannot leak between specs. Production: Requests per window per client IP on app routes (default 120). |
RATE_LIMIT_AUTH_WINDOW | Off in tests so a shared window cannot leak between specs. Production: better-auth's window for |
RATE_LIMIT_AUTH_MAX | Off in tests so a shared window cannot leak between specs. Production: Requests per auth window per client IP (default 100). |
RATE_LIMIT_IP_HEADERS | Off in tests so a shared window cannot leak between specs. Production: The header your reverse proxy sets to the client's IP ( |
GROWTHBOOK_API_HOST | Required by config validation but never contacted — AppFactory replaces FeatureFlagService with a mock. Production: feature-flags and empty by default. See GrowthBook. |
GROWTHBOOK_CLIENT_KEY | Required by config validation but never contacted — AppFactory replaces FeatureFlagService with a mock. Production: An SDK connection's client key ( Where to get it: GrowthBook → SDK Connections. |
Not in .env.example
Section titled “Not in .env.example”| Variable | Used by | Effect |
|---|---|---|
OPENOBSERVE_STREAM |
telemetry | Stream name; defaults to OTEL_SERVICE_NAME |
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS |
telemetry | Any OTLP backend instead of OpenObserve; takes precedence |
OTEL_SDK_DISABLED=true |
telemetry | Turns telemetry off |
OTEL_METRIC_EXPORT_INTERVAL |
telemetry | Metrics interval in ms (default 60000) |
INFISICAL_PROJECT_ID, INFISICAL_PATH |
yarn secrets |
See Secrets with Infisical |
INFISICAL_TOKEN, INFISICAL_API_URL |
Infisical CLI, CI | Machine token and your Infisical host |