Environment variables
A single .env file at the repository root is used by both apps:
- the server loads it at the very top of
apps/server/src/main.ts(before any entity import — see Driver locking); - the frontend scripts wrap Next.js with
dotenv -e "${ENV_FILE:-../../.env}".
ENV_FILE names another file, for a build that runs outside the repository tree or keeps its
secrets under /etc: ENV_FILE=/etc/vocab-bloom-hub/.env yarn start. The server logs which file
it loaded and exits with code 1 when an explicitly named file cannot be read (a missing default is
only a warning: the variables may come from the process environment).
Hinweis
Variables already present in the process environment always win over the file (dotenv never overrides them).
Wichtig
Use an absolute path — the server resolves a relative one from its working directory, the
frontend scripts from apps/frontend.
Variables
Hinweis
The admin credentials were once USERNAME / PASSWORD; only ADMIN_USERNAME /
ADMIN_PASSWORD are read now (a bare USERNAME collides with the variable most operating
systems set to the current user).
| Variable | Required | Default | Used by | Description |
|---|---|---|---|---|
ADMIN_USERNAME | yes | — | server | Admin login. Together with ADMIN_PASSWORD it derives the login-proof key and the JWT signing secret (see authentication.md). |
ADMIN_PASSWORD | yes | — | server | Admin password. The server refuses to start when it is missing or blank. |
DATABASE_URL | in production | SQLite fallback (dev only) | server | Database URL; the scheme selects the driver. postgres://user:pass@host:5432/db (or postgresql://) runs Postgres with the schema managed by migrations (see database.md). sqlite:<path> (e.g. sqlite:./my.sqlite, sqlite::memory:) runs better-sqlite3 with synchronize — used by the browser e2e tests for an isolated database. Any other scheme fails startup. When absent in development, TypeORM falls back to dev.sqlite at the repo root. |
DB_POOL_SIZE | no | 10 | server | Maximum connections in the Postgres pool (the pg driver's max; ignored on SQLite — it has no pool). Raise it when the pool metrics (vbh_db_pool_connections, see observability.md) show clients waiting; on a managed Postgres keep replicas × DB_POOL_SIZE under the instance's connection limit, leaving headroom for migrations and ad-hoc sessions. A whole number, at least 1; anything else fails startup. |
DB_POOL_IDLE_TIMEOUT | no | 10 | server | Seconds an idle Postgres pool connection is kept before it is closed (the pg driver's idleTimeoutMillis). 0 keeps idle connections open forever. Whole seconds; anything else fails startup. |
SERVER_PORT | no | 3010 | server, frontend, site | Port the NestJS API listens on. The frontend and site also read it as the fallback target of their /api/* forwarding route (core/apiProxy.ts) when API_INTERNAL_URL is unset. Under docker compose it is the host port the API is published on, 3240 by default. |
FRONT_PORT | no | 3000 | frontend, server | Port of the admin UI when started through the workspace scripts (yarn front:dev / yarn start:front) — wired into PORT through next.config.ts, so it covers next dev and next start alike. The server also reads it for the development CORS default. The Docker image and the systemd/PM2 examples set PORT directly instead. Under docker compose it is the host port of the admin UI, 3241 by default. |
PORT | no | 3000 / 3020 | frontend, site | What next start and the standalone server.js actually listen on. docker-compose.yml, the Dockerfiles and the systemd/PM2 examples set it explicitly; the workspace scripts derive it from FRONT_PORT / SITE_PORT, so a native start usually never sets it by hand. |
SITE_PORT | no | 3020 | site, server | Port the website listens on (yarn site:dev / yarn start:site; the host port of the site compose service). See deployment/docker.md. Under docker compose the host port defaults to 3242. |
NEXT_PUBLIC_SITE_URL | no | http://localhost:3020 | site | The public origin of the website, for the absolute URLs of its sitemaps (sitemap.xml, sitemap-words.xml), robots.txt, the canonical / hreflang links and the OpenGraph / Twitter social cards (metadataBase). Inlined at build time, so under Docker it is a build argument (docker-compose.build.yml) and has no effect on a pulled image: the published site image is built for https://vocab-bloom-hub.com (deployment/docker.md). |
CONTENT_ROOT | no | the repository root | site | Root the website reads the repository content from at build time (docs/*.md, the READMEs, the OpenAPI document; apps/site/src/content/repo.ts). Defaults to two levels above the site app — the checkout layout, which the Dockerfile also arranges — so it only needs setting for a build outside that layout. |
NEXT_PUBLIC_BASE_API_URL | no | /api | frontend, site | Base URL the browser uses for API requests (the admin UI, the site's playground and search). Inlined at build time (NEXT_PUBLIC_ prefix), so a change requires a rebuild. A relative value (the default) is resolved against the page origin — "the API is under this origin", which the reverse proxy provides. Server-side rendering uses it too unless API_INTERNAL_URL is set, and then it must be absolute. |
API_INTERNAL_URL | no | — (NEXT_PUBLIC_BASE_API_URL) | frontend, site | Runtime address the frontend and site processes themselves use for the API, without the proxy: http://server:3010/api in docker-compose.yml. Used for server-side rendering (the admin pages, the site's word pages) and for forwarding /api/* requests that reach the frontend or site origin (no reverse proxy in front); for the forwarding it falls back to http://127.0.0.1:<SERVER_PORT>/api. Never sent to the browser. |
CORS_ORIGINS | no | http://localhost:<FRONT_PORT>, http://localhost:<SITE_PORT> | server | Comma-separated list of allowed CORS origins, e.g. https://admin.example.com,https://staging.example.com. |
TRUST_PROXY | no | — (headers ignored) | server | Express trust proxy setting: a hop count (1 for one reverse proxy), loopback, an IP / CIDR list, or true. Makes rate limits and logs use the client address from X-Forwarded-For. Set it only behind a proxy; see deployment/reverse-proxy.md. |
METRICS_ENABLED | no | false | server | Serves Prometheus metrics at METRICS_PATH (process, HTTP by route template, search tiers, dictionary size, transfers, Postgres pool). Keep the endpoint off the public internet; see observability.md. |
METRICS_PATH | no | /metrics | server | Path of the metrics endpoint, outside both API surfaces. |
PUBLIC_API_ENABLED | no | true | server | Serves the public read-only prefix /api/v1. false makes those routes answer 404 (admin-only instance). See api.md. |
ADMIN_API_ENABLED | no | true | server | Serves the admin surface (/api/en, /api/settings, /api/auth). false makes those routes answer 404 (public-only instance); disabling both surfaces fails startup. |
PUBLIC_API_RATE_LIMIT | no | 100/60 | server | Requests per client IP allowed on the whole /api/v1 prefix, as <requests>/<seconds>. A batch lookup counts as one request. Anything else fails startup. |
PUBLIC_API_CACHE_MAX_AGE | no | 3600 | server | Seconds a shared cache (browser, CDN, reverse proxy) may keep a public GET answer: Cache-Control: public, max-age=<value> on every successful /api/v1 GET. 0 sends public, no-cache (revalidate on each use; the ETag makes that a bodiless 304). Anything but a non-negative integer fails startup. See api.md. |
SUGGESTIONS_RATE_LIMIT | no | 5/3600 | server | Reports per client allowed on POST /api/v1/suggestions (the Report a mistake form of the word pages), as <requests>/<seconds> — a budget separate from PUBLIC_API_RATE_LIMIT. Anything else fails startup. See api.md. |
DICTIONARY_IMPORT_DIR | no | — (server-side datasets disabled) | server | Folder the dictionary import may read datasets from (zip archives or dataset folders in the export format, one level deep), e.g. a mounted volume. Paths in import requests are resolved inside it only. Unset, the Archive tab of the import page offers the upload only, no server-side list. See offline-import.md. |
DICTIONARY_AUTO_IMPORT | no | false (true in docker-compose.yml) | server | Load the dictionary by itself on first start: when no dataset version is recorded in the settings, the server imports the newest dataset in DICTIONARY_IMPORT_DIR or, without one, the published dataset from HuggingFace — in the background, with progress in the log; GET /api/ready answers 503 importing meanwhile and 503 import_failed after a failure (the next start retries). A recorded version means nothing happens. See deployment/docker.md. |
DICTIONARY_DATASET_VERSION | no | — (the moving main) | server | Pins the automatic first-start import to one revision of the published dataset: a version tag of the HuggingFace repo (each published revision is tagged with its manifest.version, see data.md), a branch or a commit sha. Manual imports pick a revision on the import page instead. |
LOG_LEVEL | no | debug in development, else log | server | Minimum server log level: verbose / debug / log / warn / error / fatal (pino's trace / info accepted too). Unknown values fall back to the default. See observability.md. |
LOG_FORMAT | no | json in production, else pretty | server | Shape of the log lines on stdout: json — one JSON object per line for a log collector (request id, method, path, status, duration; errors with their stack) — or pretty for a terminal. Anything else fails startup. See observability.md. |
ENV_FILE | no | the root .env | both | Path of the environment file to load instead of the repository root .env (absolute path). The server exits when the named file cannot be read; see above. |
AUDIT_RETENTION_DAYS | no | 90 | server | Days the journal of admin changes is kept (the History page, GET /api/en/audit); older rows are deleted on start and daily. 0 keeps them forever. Whole days; anything else fails startup. |
SHUTDOWN_TIMEOUT | no | 30 | server | Seconds a graceful stop may take after SIGTERM / SIGINT: the listener closes, requests in flight finish, the database pool closes. Past the budget the server logs forcing exit and exits with code 1 instead of waiting for the process manager's SIGKILL. Whole seconds, at least 1; anything else fails startup. See deployment/README.md. |
NODE_ENV | no | — | both | development enables debug logging and pretty logs; production requires a postgres:// DATABASE_URL, disables the Swagger UI at /api, logs JSON and warns at every admin login that arrives over plain http (the cookie is secure whenever the request came over https, in any mode). Schema management does not depend on it: SQLite always synchronizes, Postgres always uses migrations. |
Startup validation
The server validates its configuration before Nest is created (assertRequiredConfig in
apps/server/configuration.ts) and exits with code 1 and a clear error message when:
ADMIN_USERNAMEorADMIN_PASSWORDis missing or blank — this protects against the silent fail-open where an unloaded.envleaves the credentials undefined and the password hashes as the literal string"undefined". TheADMIN_prefix is deliberate: a bareUSERNAMEis commonly set by the OS to the current system user and would silently satisfy the check;NODE_ENV=productionandDATABASE_URLis not apostgres://connection string — production never runs on SQLite silently;DATABASE_URLis set but its scheme is not recognized (postgres://,postgresql://orsqlite:<path>) — guessing the driver would silently switch how the schema is managed (auto-DDL vs migrations);ENV_FILEnames a file that cannot be read, orSHUTDOWN_TIMEOUT,LOG_FORMAT,AUDIT_RETENTION_DAYS,PUBLIC_API_RATE_LIMIT,PUBLIC_API_CACHE_MAX_AGE,SUGGESTIONS_RATE_LIMIT,DB_POOL_SIZE,DB_POOL_IDLE_TIMEOUThold values that do not parse.
The resolved database driver is logged at startup: Database: Postgres (DATABASE_URL) or
better-sqlite3 (<path>); on Postgres the next line reports the resolved pool settings
(Database pool: up to N connections …).
Hinweis
Changing the pool variables requires a server restart — they are read once, when the pool is created.
Database driver locking
Entity column types are resolved at import time inside TypeORM decorators
(checkIsPostgres()), so the driver choice is locked at the first call and stays consistent for
the lifetime of the process. assertDatabaseDriverConsistent() fails the startup when entities
were imported before the environment was loaded (e.g. a custom entry point that forgets to load
.env first). Tests that need the SQLite types import
__tests__/helpers/clearDatabaseUrl.ts before any entity for the same reason.
Example .env
The template is .env.example at the repository root: what docker compose
needs, with every optional variable commented out next to its default. The minimal files for a
native production start and for development are in the README's
getting started.