Skip to content

Environment Variables

Every variable apps/web reads, generated from the process.env reads in src/, scripts/ and db/ and kept complete by apps/web/src/lib/env-vars-doc.test.ts: a new read without a row here fails the suite, and so does a row nothing reads any more. A copy-ready file with the same names is apps/web/.env.example.

"Exposed" means a host someone other than a developer can reach — see isExposedDeployment() in src/lib/repo.ts: the Postgres backend, any NEXT_PUBLIC_THIDE_ENV other than local, or NODE_ENV=production without THIDE_ALLOW_DEV_KEYS=1. On the VPS the service reads /etc/thide-mvp-db.env through the systemd drop-in thide-mvp.service.d/database.conf (infra/postgres/RUNBOOK.md); the other variables live in the unit's environment. GET /api/v1/health reports the credential gaps below by name.

Deployment and data

Variable Read in Required on an exposed host Default Purpose
NEXT_PUBLIC_THIDE_ENV src/lib/repo.ts, src/app/api/v1/_lib/payment.ts, src/components/brand/Logo.tsx yes — production on the product host unset (shown as development in the logo badge; any value but local counts as exposed) Which deployment this is. local is the only value that keeps the dev fallbacks on; production is the only value on which a live Stripe key is accepted.
THIDE_DATA_BACKEND src/lib/repo.ts, src/lib/login-challenge.ts, scripts/ksc-preview-fixture.ts yes — pg dev pg = Postgres, dev = the JSON store under THIDE_DATA_DIR. Any other value fails closed (unknown backend).
DATABASE_URL src/lib/repo-pg.ts, src/lib/login-challenge.ts, scripts/db-migrate.ts, scripts/migrate-to-pg.ts, db/run-migrations.ts yes with pg none — the first query throws Postgres connection string. The scripts read only this name.
THIDE_PG_URL src/lib/repo-pg.ts, src/lib/login-challenge.ts, scripts/migrate-to-pg.ts no unset Overrides DATABASE_URL for the app's own pools (the cutover script sets it for its parity readback).
THIDE_DATA_DIR src/lib/repo-dev.ts, src/lib/files.ts, src/lib/email.ts, src/lib/login-challenge.ts, src/lib/upload-count-jobs.ts, scripts/seed.ts, scripts/migrate-to-pg.ts, scripts/ksc-preview-fixture.ts, src/test/node-setup.ts yes — uploaded files and the upload-count queue live here on every backend <cwd>/.dev-data Root of store.json (dev store), files/, outbox/, login-challenges.json and upload-count-jobs/. Tests always get a throwaway directory.
THIDE_ALLOW_DEV_KEYS src/lib/repo.ts never set it there unset 1 makes a local next build && next start count as unexposed so the repo's dev credentials still work. Never on a host that answers to the internet.
APP_BASE_URL src/lib/email.ts, src/lib/api-base-url.ts, src/app/studio/_lib/mutations.ts, src/app/r/[token]/actions.ts, src/app/portal/settings/actions.ts, src/app/api/v1/mandates/[id]/pay/route.ts, src/app/api/v1/request-groups/[id]/pay/route.ts yes — Studio mutations throw without it under NODE_ENV=production http://localhost:3000 (mails); the request's host (API tile) Public origin: every link in a mail, Stripe return URLs, the integrator "how to connect" tile, and the origin Studio posts its own API calls to.
PORT src/app/studio/_lib/mutations.ts no 3000 Local port Studio posts to when APP_BASE_URL is unset (never in production).
THIDE_TERMS_DIR src/lib/terms.ts no content/terms next to the app Override for a deployment that mounts the contract texts elsewhere.

| CLIENT_PORTAL_BASE_URL | src/app/portal/settings/actions.ts | no | APP_BASE_URL | Client portal origin for saved payment-method return links. |

Credentials (resolved only by src/lib/deployment-secrets.ts)

Each has a dev fallback that is honoured only on an unexposed host; on an exposed host an unset value fails closed. missingProductionSecrets() names the first three in /api/v1/health and in the boot log.

Variable Required on an exposed host Default (unexposed only) Purpose
STUDIO_DEV_KEY yes repo dev key The x-thide-studio header value (translator-side API key) and the operator bootstrap key for /api/v1/cron/acceptance.
THIDE_SESSION_SECRET yes repo dev secret HMAC secret for the portal/Studio session cookie, login codes and login-challenge code hashes.
FILE_LINK_SECRET yes repo dev secret HMAC secret behind every expiring document link.
THIDE_API_KEYS no — without it there is no bootstrap integrator key one repo dev key Comma-separated integrator keys for x-thide-key (TD/KSc bootstrap; owners create their own keys under Settings).

Studio access

Variable Read in Required on an exposed host Default Purpose
STUDIO_ACCESS_EMAILS src/app/studio/_lib/auth.ts, src/lib/email.ts no unset Comma-separated operator addresses that may sign in to any professional's Studio (owner picked under /studio/allocation) and that receive the professional mails of a request that has no owner yet.

Mail

Without RESEND_API_KEY or the four GRAPH_* values every mail lands in THIDE_DATA_DIR/outbox/ as .eml. With a live transport, a recipient must also be whitelisted or the mail still lands in the outbox (fail closed).

Variable Read in Required on an exposed host Default Purpose
GRAPH_TENANT_ID src/lib/email.ts yes (with the other three) for live mail via Microsoft 365 unset Entra tenant of the sending mailbox.
GRAPH_CLIENT_ID src/lib/email.ts same unset App registration (client-credentials flow).
GRAPH_CLIENT_SECRET src/lib/email.ts same unset Its secret.
GRAPH_SENDER src/lib/email.ts same unset Mailbox the mail is sent as. Graph wins over Resend when both are configured.
RESEND_API_KEY src/lib/email.ts alternative to Graph unset Resend transport.
THIDE_MAIL_WHITELIST src/lib/email.ts yes for any mail to leave unset — nothing leaves Comma-separated addresses or @domain suffixes allowed to receive live mail.
MAIL_FROM src/lib/email.ts no THIDE <no-reply@thide.io> From header on Resend sends.
THIDE_REPLY_TO src/lib/email.ts no TRANSLATOR_EMAIL Reply-To until an inbound pipeline exists.
THIDE_SUPPORT_EMAIL src/lib/email.ts no support@thide.io The "write to us" address in mails.
TRANSLATOR_EMAIL src/lib/email.ts yes if the legacy professional should be mailed at a real address translator@dev.THIDE.local (outbox only) Overrides the legacy professional's own address for her notifications.
TRANSLATOR_EMAIL_CC src/lib/email.ts no unset Comma-separated copies of the legacy professional's mails (TD, 1 Sep), each through the whitelist.

Payments

Variable Read in Required on an exposed host Default Purpose
STRIPE_SECRET_KEY src/lib/stripe.ts, src/app/api/v1/_lib/payment.ts, src/app/api/v1/webhooks/stripe/route.ts, src/app/studio/layout.tsx yes for payments — an exposed host without it refuses to start one; sk_live_ is refused unless NEXT_PUBLIC_THIDE_ENV=production unset — dev fallback payment on unexposed hosts only Stripe API key (Checkout, refunds, Connect, the Studio payouts banner).
STRIPE_WEBHOOK_SECRET src/app/api/v1/webhooks/stripe/route.ts yes with Stripe unset — the webhook answers 503 Signing secret the webhook verifies the raw body against.
THIDE_PAYOUT_POLICY src/lib/payout-policy.ts, src/lib/escrow.ts no escrow Named policy from PAYOUT_POLICIES; anything but escrow blocks releases (a product and legal decision, see the module comment).

Machine translation

Variable Read in Required on an exposed host Default Purpose
THIDE_MT_ENABLED src/lib/deepl.ts no unset (off) 1 offers the machine tier — only together with DEEPL_API_KEY.
DEEPL_API_KEY src/lib/deepl.ts with THIDE_MT_ENABLED=1 unset DeepL key; a :fx suffix selects the free endpoint.

OCR and counting

Variable Read in Required on an exposed host Default Purpose
THIDE_OCR_DISABLED src/lib/count.ts, scripts/check-ocr-runtime.mjs no unset (OCR on) 1 skips OCR for scans (counts pause for the professional); the preflight script reports it as a failure.
THIDE_OCR_REVIEW_ENABLED src/lib/upload-count-jobs.ts no unset (off) 1 runs the visual OCR page review; part of COUNT_ENGINE_VERSION, so flipping it invalidates the count cache.
THIDE_OCR_REVIEW_MODEL src/lib/upload-count-jobs.ts with the review unset Model id for the visual review; also part of COUNT_ENGINE_VERSION.
ANTHROPIC_API_KEY src/lib/upload-count-jobs.ts with the review unset — no reviewer is created API key the visual review calls with.
THIDE_REPEAT_SECTION_CORRECTION src/lib/count-policy.ts no unset (full count) 1 restores the retained repeat-section deduction; part of both count-cache keys, so results of the other policy are not reused. Existing quotes and manual overrides are not repriced. Restart the counting workers after changing it.
THIDE_WEASYPRINT_BIN src/lib/pdf.ts, src/test/node-setup.ts no the VPS venv binary, else weasyprint on PATH PDF renderer for receipts, invoices and credit notes; tests pin a stub.

| THIDE_TOKEN_LEDGER_MODULE | src/lib/ocr-usage.ts | no | unset | Absolute path to the local Python token ledger module. Without it, usage remains in the durable OCR usage outbox. | | THIDE_TOKEN_LEDGER_DB | src/lib/ocr-usage.ts | no | ledger module default | Optional local ledger database path forwarded to the usage bridge. |

Operations

Variable Read in Required on an exposed host Default Purpose
THIDE_INTERNAL_TICK src/instrumentation.ts no unset (on) 0 stops the in-process hourly acceptance sweep and deferred-count resume for a host that schedules POST /api/v1/cron/acceptance itself. The upload-count resume and the five-minute tick (callback retries, idempotency and login-challenge purge) keep running: they have no external entry point.
THIDE_PROFESSIONAL_ROUTING src/lib/professional-routing.ts no unset (off) 1 activates automatic professional routing (rollout gate; capability evidence first).
THIDE_BEHIND_CLOUDFLARE src/lib/rate-limit.ts no unset 1 trusts cf-connecting-ip for the per-IP meters even when the peer is not a known Cloudflare range.
THIDE_RATE_LIMIT_FORCE src/lib/rate-limit.ts no unset 1 turns the rate limiters on under NODE_ENV=test (used by the limiter tests).

Invoicing by THIDE (e-invoicing, B-01)

Operations detail, webhook registration and the production checklist: docs/mvp-sept2026/einvoice-ops.md. Nothing here is required for a PDF-only deployment: with the stubs selected every path runs without a network.

Variable Read in Required on an exposed host Default Purpose
THIDE_ISSUER_REGISTRY src/lib/issuer-registry.ts recommand once professionals are verified at the access point stub Who creates and reads the professionals' sending companies and answers the recipient lookup: stub (in-process, verified at once) or recommand. recommand needs the three RECOMMAND_* credentials and fails loudly, naming the variable, when one is missing.
THIDE_DELIVERY_SENDER src/lib/einvoice-sender.ts recommand once e-invoices leave the house stub Which access point sends the UBL: stub (answers per THIDE_STUB_SENDER_OUTCOME) or recommand. Same credentials as the registry — one key pair, one team.
RECOMMAND_BASE_URL src/lib/issuer-registry.ts, src/lib/einvoice-sender.ts yes with either recommand selection none — the factory throws The access point's API origin (https://app.recommand.eu in production; the playground URL is whatever the playground team gives).
RECOMMAND_API_KEY_ID src/lib/issuer-registry.ts, src/lib/einvoice-sender.ts yes with either recommand selection none HTTP Basic user of the Recommand key pair. Never logged.
RECOMMAND_API_SECRET src/lib/issuer-registry.ts, src/lib/einvoice-sender.ts yes with either recommand selection none HTTP Basic password of the key pair. Never logged, never in an error message.
RECOMMAND_THIDEWORKS_COMPANY_ID src/lib/einvoice-delivery.ts, src/lib/ubl/machine-invoice-ubl.ts yes once THIDE_MT_EINVOICE is on (D4) unset thideworks BV's own sending company at the access point. Unset, a THIDE-MT delivery attempt fails issuer_not_verified and the register row falls back to channel pdf — the PDF the client holds stays the original.
RECOMMAND_WEBHOOK_SECRET src/app/api/v1/webhooks/recommand/route.ts yes once the webhook is registered unset HMAC-SHA256 key for X-Signature: sha256=<hex> over the raw body. Unset, the route answers 501 and records nothing; must equal the secret registered at Recommand.
THIDE_EINVOICE_STATUS_POLL_MINUTES src/lib/einvoice-delivery.ts no 15 How long a sent row is left alone before the five-minute status sweep asks the access point; the webhook normally settles the row first. Must be a positive number.
THIDE_MT_EINVOICE src/lib/machine-einvoice.ts no — stays off until D4 and the KSc sign-off of the MT note unset (off) 1 or true builds, stores and queues the THIDE-MT machine-tier e-invoice next to its PDF. Off, the machine invoice stays PDF-only and its register row keeps channel pdf.
THIDE_STUB_SENDER_OUTCOME src/lib/einvoice-sender.ts never delivered Development only: forces the stub sender's answer (delivered, unreachable, validation, pending, unavailable) so every outbox branch can be exercised without a network. Ignored when the sender is recommand.
THIDE_VIES_STUB src/lib/vies.ts never unset (VIES) 1 replaces the VIES REST check of a Belgian VAT number at opt-in by an in-process check-digit stub, for development and tests.

Set by the runtime, not by configuration

Variable Read in Meaning
NODE_ENV src/lib/repo.ts, src/lib/rate-limit.ts, src/lib/upload-count-jobs.ts, src/app/studio/_lib/mutations.ts, src/app/studio/allocation/actions.ts, src/instrumentation.ts production under next start (exposed unless THIDE_ALLOW_DEV_KEYS=1), development under next dev (wider sign-in meter), test under vitest (limiters off).
NEXT_RUNTIME src/instrumentation.ts Next sets nodejs / edge; the timers only start on the node runtime.
VITEST src/instrumentation.ts Set by vitest; no background timers under test.

Test harness only

Variable Read in Meaning
THIDE_PG_TEST_URL src/lib/repo-contract.test.ts, src/lib/login-challenge.test.ts, src/lib/db-migrate.test.ts, src/lib/professional-identity*.test.ts, src/app/api/v1/health/route.test.ts Disposable Postgres the PG suites run against (npm run test:pg sets it). Never production: the harness truncates.
COUNT_NATIVE_PROBE_ROOT, COUNT_NATIVE_PROBE_SCENARIO src/lib/count-native-lifecycle.test.ts Child-process probe of the native count lifecycle.
THIDE_STUDIO_KEY src/app/api/v1/_lib/envelope-redaction.test.ts Studio header value one test sends; empty by default.
PATH src/lib/count-native-lifecycle.test.ts Passed through to the probe child process by that test only.
RECOMMAND_PLAYGROUND scripts/recommand-playground-e2e.ts 1 is the explicit consent the playground end-to-end script demands before it talks to an access point (it cannot tell a playground from production by the URL).
UPDATE_GOLDEN src/lib/ubl/ubl-documents.test.ts 1 regenerates the UBL goldens (UPDATE_GOLDEN=1 npx vitest run src/lib/ubl); the diff is the review.