---
title: "Configuration reference"
description: "All 103 environment variables Veefore reads, with their defaults."
label: "Configuration"
generated: ".env.example"
---


Veefore is configured entirely through the environment. There is no configuration file the services read at runtime and no settings table that can disagree with the process that booted. Configuration is validated at startup; an invalid value stops the process rather than degrading it.

:::security Secrets belong in a secret manager
The cryptographic keys below are the ones that matter: data encryption, platform signing, session signing and the audit chain. In anything beyond local development, inject them from a secret manager. Rotating the signing key invalidates issued passports; rotating the data encryption key requires re-sealing credential material.
:::

Copy `.env.example` to `.env` to start. `npm run dev` reads the root `.env`, and so does the console’s Vite config, so the ports stay consistent across every process.

The **Ships as** column is the value in `.env.example`, which is also the value the services fall back to when the variable is unset. Where the example file carries a commented-out alternative, it is shown underneath.

## Runtime

| Variable | Ships as | Notes |
| --- | --- | --- |
| `VEEFORE_ENV` | `development` | development \| test \| staging \| production |
| `NODE_ENV` | `development` |  |
| `LOG_LEVEL` | `info` |  |

## Control Plane API

| Variable | Ships as | Notes |
| --- | --- | --- |
| `CONTROL_PLANE_HOST` | `0.0.0.0` |  |
| `CONTROL_PLANE_PORT` | `4000` |  |
| `CONTROL_PLANE_PUBLIC_URL` | `http://localhost:4000` | Production: https://api.veefore.com |
| `CORS_ALLOWED_ORIGINS` | `http://localhost:5173` | Platforms that assign a port (Railway, Heroku, Cloud Run) pass it as PORT. Leave CONTROL_PLANE_PORT and GATEWAY_PORT unset there and the service binds PORT; set either one and it wins, which is what keeps local development on 4000 and 4010 regardless of what the shell exports. Alternative: `PORT=` Comma-separated list of browser origins allowed to call the control plane. Production: https://console.veefore.com,https://veefore.com Mostly a backstop: the console reaches /v1 same-origin (Vite's proxy in development, an edge rewrite in production), so the browser sends no Origin header and CORS never engages. The list is also what social sign-in matches a forwarded host against when choosing a redirect base. |
| `CONSOLE_PUBLIC_URL` | _empty_ | Where people open the console. The CLI sends engineers here to sign in from the browser. Empty means the first CORS_ALLOWED_ORIGINS entry. Production: https://console.veefore.com |
| `MARKETING_PUBLIC_URL` | _empty_ | Where the public marketing site is served. Separate deployment, separate origin (apps/marketing). Production: https://veefore.com |
| `SESSION_HINT_COOKIE_DOMAIN` | _empty_ | `Domain` for `veefore_signed_in` — a non-secret cookie holding the single character `1`, which is how the marketing site on the apex domain knows to hand a signed-in visitor over to the console. Nothing on the server reads it and forging it grants nothing. The session cookie itself is NEVER domain-scoped: it stays HttpOnly and host-only on the console's origin. See apps/control-plane/src/http/session-cookies.ts. Empty keeps the hint host-only too, which is right for local development and for any deployment serving the site and the console from one host. Set it to the registrable domain they share when they are on sibling hosts. Production: .veefore.com |
| `VEEFORE_HOSTED` | `false` | true only on the hosted Veefore service that the published CLI connects to by default. Self-hosted installs leave it false, and setup instructions then show engineers this deployment's address to pass to `veefore --control-plane` once. |

## Gateway (Enforcement Plane)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `GATEWAY_HOST` | `0.0.0.0` |  |
| `GATEWAY_PORT` | `4010` |  |
| `GATEWAY_PUBLIC_URL` | `http://localhost:4010` | Production: https://gateway.veefore.com |
| `GATEWAY_AUDIENCE_ALIASES` | _empty_ | Comma-separated extra `aud` values the gateway accepts besides GATEWAY_PUBLIC_URL, for a gateway reachable under several addresses. |
| `GATEWAY_EGRESS_TIMEOUT_MS` | `15000` | Hard ceiling on outbound tool execution time (ms) |
| `GATEWAY_ALLOW_REAL_EGRESS` | `false` | Set to `true` to allow the gateway to make real outbound HTTP calls. In development/demo this stays false and the simulated tool runtime is used. |

## Database

| Variable | Ships as | Notes |
| --- | --- | --- |
| `DATABASE_URL` | _empty_ | When DATABASE_URL is set, the `pg` driver is used (production path). When it is empty, Veefore boots an embedded PGlite Postgres, which is per-process: the control plane, the gateway and the worker each get their own private copy and see none of each other's writes. Correct for isolated tests, wrong for running the stack. Set this for anything multi-process. Alternative: `postgres://veefore:veefore@localhost:5432/veefore` |
| `DATABASE_SSL` | `false` |  |
| `DATABASE_POOL_MAX` | `20` |  |
| `DATABASE_STATEMENT_TIMEOUT_MS` | `15000` |  |
| `PGLITE_DATA_DIR` | `memory://` | Directory used by the embedded PGlite instance. `memory://` = ephemeral. |
| `DATABASE_ENABLE_RLS` | `false` | Enable PostgreSQL row level security policies (requires the `pg` driver). |

## Redis (rate limiting, velocity counters, approval pub/sub)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `REDIS_URL` | `redis://localhost:6379` | Leave empty to use the in-process store (single node development only). Alternative: `redis://localhost:6379` |

## Cryptography

| Variable | Ships as | Notes |
| --- | --- | --- |
| `VEEFORE_DATA_ENCRYPTION_KEY` | _empty_ | 32-byte base64 key used to envelope-encrypt sensitive metadata at rest. Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" |
| `VEEFORE_SIGNING_PRIVATE_KEY` | _empty_ | Base64 PKCS8 Ed25519 private key used to sign agent identity tokens and passports. Generate with: npm run -s migrate -- --print-keypair |
| `VEEFORE_SIGNING_KEY_ID` | _empty_ |  |
| `VEEFORE_SESSION_SECRET` | _empty_ | Secret used to sign human browser sessions. |
| `VEEFORE_AUDIT_CHAIN_SECRET` | _empty_ | Audit hash-chain pepper. Rotating this breaks historical verification. |

## Session / auth policy

| Variable | Ships as | Notes |
| --- | --- | --- |
| `SESSION_TTL_SECONDS` | `43200` |  |
| `SESSION_IDLE_TIMEOUT_SECONDS` | `3600` |  |
| `STEP_UP_TTL_SECONDS` | `300` |  |
| `AGENT_TOKEN_TTL_SECONDS` | `900` |  |
| `PASSPORT_TTL_SECONDS` | `2592000` |  |
| `MAX_LOGIN_ATTEMPTS` | `10` |  |
| `LOGIN_LOCKOUT_SECONDS` | `900` |  |

## Private beta

| Variable | Ships as | Notes |
| --- | --- | --- |
| `SIGNUP_ACCESS` | `invite_only` | invite_only (default): a new organization needs an invitation, issued when a platform operator approves a waitlist application (/waitlist). The link is e-mailed, works for one sign-up with the applicant's address, and expires 24 hours after it is sent. `open` lets anyone sign up. |
| `PLATFORM_SUPERADMIN_EMAIL` | _empty_ | Platform admin (admin.veefore.com). Admin accounts are separate from customer accounts: they sign in only on the admin site, always with a password and an authenticator code, and can never sign in to the console. The superadmin below is created on start and kept in step with these values, which is how a new deployment gets its first way in. Only the password HASH goes here. Generate one (and a strong password) with: npm run admin:password Changing the hash signs the superadmin out everywhere. Further admins are added by a superadmin in the admin site, under Admins. |
| `PLATFORM_SUPERADMIN_NAME` | `Superadmin` |  |
| `PLATFORM_SUPERADMIN_PASSWORD_HASH` | _empty_ |  |
| `ADMIN_PUBLIC_URL` | `http://localhost:5175` | Where the admin site is served. Its origin may call the API. Production: https://admin.veefore.com |
| `PLATFORM_ADMIN_SESSION_TTL_SECONDS` | `28800` | Admin sessions: absolute lifetime and idle timeout, in seconds. |
| `PLATFORM_ADMIN_IDLE_TIMEOUT_SECONDS` | `1800` |  |
| `PLATFORM_ADMIN_ALLOWED_IPS` | _empty_ | Optional comma-separated IPs or CIDRs the platform admin accepts requests from (e.g. 203.0.113.7,10.0.0.0/8). Empty means any network. Only set this when your edge proxy overwrites X-Forwarded-For with the real client address. |

## Social sign-in (Google, GitHub). A provider appears on the sign-in page only when both its client id and secret are set. Register this redirect URI with the provider, exactly: <OAUTH_REDIRECT_BASE_URL>/v1/auth/oauth/google/callback <OAUTH_REDIRECT_BASE_URL>/v1/auth/oauth/github/callback The base is the browser-facing origin that serves /v1 (normally the console, which proxies it). Empty means CONSOLE_PUBLIC_URL. Must be https in production. Keep the secrets in your secret manager, not in this file.

| Variable | Ships as | Notes |
| --- | --- | --- |
| `OAUTH_REDIRECT_BASE_URL` | _empty_ |  |
| `OAUTH_GOOGLE_CLIENT_ID` | `REPLACE_WITH_GOOGLE_CLIENT_ID` |  |
| `OAUTH_GOOGLE_CLIENT_SECRET` | `REPLACE_WITH_GOOGLE_CLIENT_SECRET` |  |
| `OAUTH_GITHUB_CLIENT_ID` | `REPLACE_WITH_GITHUB_CLIENT_ID` |  |
| `OAUTH_GITHUB_CLIENT_SECRET` | `REPLACE_WITH_GITHUB_CLIENT_SECRET` |  |
| `OAUTH_HTTP_TIMEOUT_MS` | `10000` |  |

## Passkeys and security keys (WebAuthn) Passkeys are bound to WEBAUTHN_RP_ID (a registrable domain such as veefore.example.com). Changing it later invalidates every registered passkey, so set it once. Defaults: the hostname of CONSOLE_PUBLIC_URL, and that URL plus CORS_ALLOWED_ORIGINS as the accepted origins. Browsers only allow passkeys on https origins and on http://localhost.

| Variable | Ships as | Notes |
| --- | --- | --- |
| `WEBAUTHN_RP_ID` | _empty_ |  |
| `WEBAUTHN_RP_NAME` | `Veefore` |  |
| `WEBAUTHN_ORIGINS` | _empty_ |  |

## Credential broker

| Variable | Ships as | Notes |
| --- | --- | --- |
| `CREDENTIAL_BROKER_PROVIDER` | `local` | local \| aws-secrets-manager \| azure-key-vault \| gcp-secret-manager \| vault |
| `CREDENTIAL_DEFAULT_TTL_SECONDS` | `600` |  |
| `AWS_REGION` | _empty_ |  |
| `AWS_SECRETS_MANAGER_PREFIX` | `veefore/` |  |
| `AZURE_KEY_VAULT_URL` | _empty_ |  |
| `GCP_PROJECT_ID` | _empty_ |  |
| `VAULT_ADDR` | _empty_ |  |
| `VAULT_NAMESPACE` | _empty_ |  |

## Observability

| Variable | Ships as | Notes |
| --- | --- | --- |
| `OTEL_ENABLED` | `false` |  |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` |  |
| `OTEL_SERVICE_NAME` | `veefore` |  |
| `METRICS_ENABLED` | `true` |  |

## Notifications / integrations (optional)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `EMAIL_TRANSPORT` | `log` | resend \| log Every e-mail (verification codes, password resets and security notices, waitlist and beta invitations, team invitations, alerts) is sent through the Resend API with EMAIL_TRANSPORT=resend. `log` never sends anything: outside staging/production it keeps the last messages for GET /v1/dev/outbox (local machine only); in staging/production it is refused. |
| `RESEND_API_KEY` | _empty_ | From https://resend.com/api-keys ("Sending access" is enough). Secret. |
| `RESEND_API_URL` | `https://api.resend.com` |  |
| `EMAIL_HTTP_TIMEOUT_MS` | `10000` |  |
| `EMAIL_FROM` | `no-reply@veefore.com` | Must be an address on a domain verified in Resend (SPF + DKIM). |
| `EMAIL_FROM_NAME` | `Veefore` |  |
| `EMAIL_SUPPORT_ADDRESS` | _empty_ | Shown in e-mail footers and used as Reply-To unless EMAIL_REPLY_TO is set. |
| `EMAIL_REPLY_TO` | _empty_ |  |
| `EMAIL_COMPANY_ADDRESS` | _empty_ | Postal address in the footer of every e-mail. Empty hides the line. |
| `EMAIL_LOGO_URL` | _empty_ | Logo in the e-mail header, as an absolute http(s) URL of a PNG or JPG (SVG is not supported by most mail clients). Empty serves the bundled mark from the console at <CONSOLE_PUBLIC_URL>/brand/veefore-mark.png, which a mail client can only load once the console is reachable publicly. |
| `VITE_TERMS_URL` | _empty_ | Console legal links shown on the sign-in and sign-up pages. Absolute https:// URLs or same-origin paths. Empty hides the link. |
| `VITE_PRIVACY_URL` | _empty_ |  |
| `VITE_SECURITY_URL` | _empty_ |  |
| `SLACK_WEBHOOK_URL` | _empty_ |  |
| `TEAMS_WEBHOOK_URL` | _empty_ |  |

## Billing / usage metering (Veefore SaaS itself)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `USAGE_METERING_ENABLED` | `true` |  |
| `USAGE_ROLLUP_INTERVAL_SECONDS` | `300` |  |

## Front ends (Vite — only VITE_* vars reach the browser; never put secrets here)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `VITE_CONSOLE_URL` | `http://localhost:5173` | VITE_* values are compiled into the bundle at build time. Changing one needs a rebuild and redeploy; restarting a server does nothing. There is deliberately no API base URL. The console calls /v1 and /scim as same-origin paths, proxied to the control plane by the edge (Vite in development, a Vercel rewrite in production). That is what keeps the session cookie host-only and SameSite=Lax rather than a cross-site cookie, and keeps CORS out of the browser's path. Adding one would undo all of it. Where the marketing site sends people to sign in, and where the console's "home" link goes. The two apps run on different ports locally and different origins in production. Production: https://console.veefore.com and https://veefore.com |
| `VITE_MARKETING_URL` | `http://localhost:5174` |  |
| `VITE_DOCS_URL` | `http://localhost:4800` | Where the marketing site's "Docs" links go. The docs are their own static site on their own origin, not a path on the marketing site. Production: https://docs.veefore.com (veefore.com/docs redirects there) |
| `VEEFORE_CONSOLE_PORT` | `5173` | Dev-server ports. Bound with strictPort, so a collision is a startup error rather than a silent move to another port. Change CORS_ALLOWED_ORIGINS to match the console's, or the browser refuses every request with no useful error. |
| `VEEFORE_CONSOLE_PREVIEW_PORT` | `4173` |  |
| `VEEFORE_MARKETING_PORT` | `5174` |  |
| `VEEFORE_MARKETING_PREVIEW_PORT` | `4174` |  |
| `VEEFORE_ADMIN_PORT` | `5175` |  |
| `VEEFORE_ADMIN_PREVIEW_PORT` | `4175` |  |
| `VEEFORE_CONTROL_PLANE_URL` | _empty_ | Proxy target for the dev server. Defaults to CONTROL_PLANE_PORT on loopback. |

## Documentation site (npm run docs:dev)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `VEEFORE_DOCS_PORT` | `4800` | Serves docs/dist on the loopback interface only. It is unauthenticated content from a working tree and has no business being reachable remotely. |

## Example downstream service (examples/real-tool-server.mjs)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `TOOL_SERVER_PORT` | `4500` | A real HTTP service on loopback with its own API key, used to observe what actually reaches a downstream under each enforcement mode. The key belongs to the credential broker, never to the agent. |
| `TOOL_SERVER_API_KEY` | `downstream-secret-key` |  |

## Veefore-hosted AI code assist (`veefore integrate --assist=veefore`)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `VEEFORE_AI_PROVIDER` | _empty_ | Off unless a provider and key are set. Developers can always use their own key (--assist=own) or make the change by hand (--assist=manual) instead. Source sent here is used for the request only; it is never stored or logged. |
| `VEEFORE_AI_API_KEY` | _empty_ |  |
| `VEEFORE_AI_MODELS` | _empty_ | Comma-separated model ids developers may pick from, e.g. the ids your provider's /v1/models lists. |
| `VEEFORE_AI_BASE_URL` | _empty_ | Optional proxy in front of the provider. |

## Automatic exchange rates (optional)

| Variable | Ships as | Notes |
| --- | --- | --- |
| `VEEFORE_FX_PROVIDER_URL` | _empty_ | Off unless a URL is set; admins can always enter manual rates in Admin › Settings, and a manual rate always overrides an automatic one for the same pair. Veefore sends one GET to this URL per refresh (daily, and on demand) and nothing else: no tenant data is ever included. The response must be {"base":"USD","date":"YYYY-MM-DD","rates":{"INR":83.1,"EUR":0.92}} which is what frankfurter.app and exchangerate.host return, e.g. Alternative: `VEEFORE_FX_PROVIDER_URL=https://api.frankfurter.app/latest?from=USD` |
| `VEEFORE_FX_PROVIDER_API_KEY` | _empty_ | Optional key, sent in the header named below. |
| `VEEFORE_FX_PROVIDER_API_KEY_HEADER` | `apikey` |  |
| `VEEFORE_FX_PROVIDER_TIMEOUT_MS` | `10000` |  |
