Vocab Bloom Hub
Diese Seite ist nur auf Englisch verfügbar.

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

VariableRequiredDefaultUsed byDescription
ADMIN_USERNAMEyes—serverAdmin login. Together with ADMIN_PASSWORD it derives the login-proof key and the JWT signing secret (see authentication.md).
ADMIN_PASSWORDyes—serverAdmin password. The server refuses to start when it is missing or blank.
DATABASE_URLin productionSQLite fallback (dev only)serverDatabase 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_SIZEno10serverMaximum 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 — and, on an instance that holds several datasets, for the reads of the ones that are not served: up to four connections each (datasets.md). A whole number, at least 1; anything else fails startup.
DB_POOL_IDLE_TIMEOUTno10serverSeconds 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_PORTno3010server, frontend, sitePort 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_PORTno3000frontend, serverPort 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.
PORTno3000 / 3020frontend, siteWhat 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_PORTno3020site, serverPort 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_URLnohttp://localhost:3020siteThe 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).
SITE_VERIFICATION_GOOGLEno—siteThe token of the google-site-verification meta tag — the "HTML tag" method of Google Search Console — for verifying the website's origin. Like NEXT_PUBLIC_SITE_URL it is read at build time (the start page that carries the tag is prerendered), so under Docker it is a build argument of docker-compose.build.yml; a pulled image carries none. DNS verification needs no variable (deployment/docker.md).
SITE_VERIFICATION_BINGno—siteThe token of the msvalidate.01 meta tag of Bing Webmaster Tools, read at build time like SITE_VERIFICATION_GOOGLE.
SITE_VERIFICATION_YANDEXno—siteThe token of the yandex-verification meta tag of Yandex Webmaster, read at build time like SITE_VERIFICATION_GOOGLE.
CONTENT_ROOTnothe repository rootsiteRoot 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_URLno/apifrontend, siteBase 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_URLno— (NEXT_PUBLIC_BASE_API_URL)frontend, siteRuntime 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_ORIGINSnohttp://localhost:<FRONT_PORT>, http://localhost:<SITE_PORT>serverComma-separated list of allowed CORS origins, e.g. https://admin.example.com,https://staging.example.com.
TRUST_PROXYno— (headers ignored)serverExpress 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_ENABLEDnofalseserverServes 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_PATHno/metricsserverPath of the metrics endpoint, outside both API surfaces.
PUBLIC_API_ENABLEDnotrueserverServes the public read-only prefix /api/v1. false makes those routes answer 404 (admin-only instance). See api.md.
ADMIN_API_ENABLEDnotrueserverServes 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_LIMITno100/60serverRequests per client IP allowed on the whole /api/v1 prefix, as <requests>/<seconds>. A batch lookup counts as one request. Anything else fails startup.
INTERNAL_API_TOKENno— (no exemption)bothA secret shared by the server and the website. The site's server-side requests to the API — the word pages, the headword walk behind sitemap-words.xml and /word/browse — carry it as X-Internal-Token and are not counted against PUBLIC_API_RATE_LIMIT; without it they share one budget with the walk, and a word page rendered during it fails. 16+ characters (openssl rand -hex 32), shorter fails startup. Set it whenever the site profile runs.
PUBLIC_API_CACHE_MAX_AGEno3600serverSeconds a shared cache (browser, CDN, reverse proxy) may keep a public GET answer: Cache-Control: public, max-age=<value>, stale-while-revalidate=<value> on every successful /api/v1 GET (a shared cache may serve the stale answer for as long again while it revalidates). 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_LIMITno5/3600serverReports 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_DIRno— (server-side datasets disabled)serverFolder 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 an import offers the upload only, no server-side list. See offline-import.md.
DICTIONARY_AUTO_IMPORTnofalse (true in docker-compose.yml)serverLoad 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_VERSIONno— (the moving main)serverPins 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 in the import of the project's dataset instead. This setting does not select an OpenGloss release; its download links are pinned in the catalog.
LOG_LEVELnodebug in development, else logserverMinimum 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_FORMATnojson in production, else prettyserverShape 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_FILEnothe root .envbothPath 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_DAYSno90serverDays the journal of what was done on the instance is kept — imports, settings, switches of the dataset, verdicts on reports (the Events tab of 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. The history of the edits of the dictionary is a part of the data and is kept for good (datasets.md).
UPDATE_CHECKnotrueserverThe update notice of the admin UI: the server asks GitHub for the latest stable release a few times a day (cached six hours) and the admin UI says when it is newer than the running version — a notice only, the instance never updates itself. The same switch covers the notice of a newer file of a source on the datasets page: the sources of the installed datasets are asked once a day (datasets.md). false disables update-check requests, not explicit installation downloads or the first-start import. OpenGloss is pinned in the catalog and has no newer-release check. Anything but a boolean word fails startup. See upgrading.md.
SHUTDOWN_TIMEOUTno30serverSeconds 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_ENVno—bothdevelopment 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.

Semantic search (optional)

SEMANTIC_SEARCH_ENABLED defaults to false and independently enables the semantic API and website mode. Starting a model or importing vectors does not enable search. The website reads this switch from its API; it does not need its own environment variable.

VariableDefaultPurpose
EMBEDDINGS_URLNative: http://localhost:11434; Compose: http://embeddings:11434Ollama HTTP(S) origin, including a remote LAN host.
EMBEDDINGS_MODELembeddinggemma-2:latestExact model name used by the imported profile.
EMBEDDINGS_REVISIONunsetRequired for enabled search: immutable 64-character model digest.
EMBEDDINGS_DIMENSIONS768Imported profile/query vector dimension (1–2,000).
EMBEDDINGS_IMAGEollama/ollama:0.40.8Compose-only runtime image; independent of the model digest.

See semantic-search.md for external vector files, API behavior, limits, unavailable errors and Docker/remote configuration. The embeddings Compose profile starts a private Ollama service with persistent model storage; its model download and PostgreSQL extension activation are explicit operator actions.

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_USERNAME or ADMIN_PASSWORD is missing or blank — this protects against the silent fail-open where an unloaded .env leaves the credentials undefined and the password hashes as the literal string "undefined". The ADMIN_ prefix is deliberate: a bare USERNAME is commonly set by the OS to the current system user and would silently satisfy the check;
  • NODE_ENV=production and DATABASE_URL is not a postgres:// connection string — production never runs on SQLite silently;
  • DATABASE_URL is set but its scheme is not recognized (postgres://, postgresql:// or sqlite:<path>) — guessing the driver would silently switch how the schema is managed (auto-DDL vs migrations);
  • ENV_FILE names a file that cannot be read, or SHUTDOWN_TIMEOUT, LOG_FORMAT, AUDIT_RETENTION_DAYS, UPDATE_CHECK, PUBLIC_API_RATE_LIMIT, PUBLIC_API_CACHE_MAX_AGE, INTERNAL_API_TOKEN, SUGGESTIONS_RATE_LIMIT, DB_POOL_SIZE, DB_POOL_IDLE_TIMEOUT hold 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.