Vocab Bloom Hub
هذه الصفحة متاحة بالإنجليزية فقط.

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

ملاحظة

Variables already present in the process environment always win over the file (dotenv never overrides them).

مهم

Use an absolute path — the server resolves a relative one from its working directory, the frontend scripts from apps/frontend.

Variables

ملاحظة

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_USERNAMEyesserverAdmin login. Together with ADMIN_PASSWORD it derives the login-proof key and the JWT signing secret (see authentication.md).
ADMIN_PASSWORDyesserverAdmin 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. 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).
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.
PUBLIC_API_CACHE_MAX_AGEno3600serverSeconds 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_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 the import page 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 on the import page instead.
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 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_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_ENVnobothdevelopment 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_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, PUBLIC_API_RATE_LIMIT, PUBLIC_API_CACHE_MAX_AGE, 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 …).

ملاحظة

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.