Skip to content

Environment Variable Naming Convention

Status: Decision recorded for issue #168 Phase B. Companion doc: ENV_VARS.md — current inventory. Implementation: This doc is decision-only. Renames happen one domain at a time in Phase F, each behind a shim that warns on legacy names. No rename ships before Phase C (boot validation hardening) lands.


  1. Predictable prefixes — reading a var name should hint at the subsystem that owns it.
  2. Public/secret legible by nameVITE_PUBLIC_* is the only browser-bundled prefix; everything else is server-side.
  3. One key per concept — collapse aliases (AUTH_SECRET vs BETTER_AUTH_SECRET, DATABASE_URL vs DATABASE_HOST/USER/…).
  4. Backwards-compatible rollout — shim helper readEnv(newName, ...fallbacks) warns when a legacy name is hit. Old names stay readable for one release per domain.

Non-goals: renaming everything in one PR, removing all aliases the moment Phase F lands, or breaking the Coolify deploy in mid-cycle.


PrefixOwnerExamples (target names)Notes
PLATFORM_*Cross-cutting platform configPLATFORM_API_URL, PLATFORM_DASHBOARD_URL, PLATFORM_APP_ENV, PLATFORM_EMAIL, PLATFORM_EMAIL_PASSWORDTop-level config that doesn’t belong to a single subsystem. apps/cli already uses this prefix — extend to apps/api/dashboard.
AUTH_*better-auth + session managementAUTH_SECRET, AUTH_URLCollapse BETTER_AUTH_* into this. Accept legacy BETTER_AUTH_* via shim.
DB_* (or keep DATABASE_URL only)Postgres connectionDATABASE_URLDrop DATABASE_HOST/USER/PASSWORD/NAME/PORT from the schema. Standardize on DATABASE_URL.
LLM_*LLM providers and model overridesLLM_PROVIDER, LLM_CLINE_API_KEY, LLM_ANTHROPIC_API_KEY, LLM_OPENROUTER_API_KEY, LLM_EXTRACT_URLS_MODEL, LLM_ANALYZE_CONTENT_MODELNew LLM_PROVIDER enum (cline|anthropic|openrouter|auto, default auto) makes precedence explicit.
NOTION_*Notion integrationNOTION_INTEGRATION_TOKEN, NOTION_DATABASE_IDAlready consistent. Both unused at runtime now that credentials come from a connected notion:// channel — kept as schema-declared for now.
GMAIL_*Gmail OAuth / service accountGMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN, GMAIL_SERVICE_ACCOUNT_PATH, GMAIL_USER_EMAIL (rename of MY_EMAIL_ADDRESS)Already consistent except MY_EMAIL_ADDRESS outlier.
RESEND_*Resend email APIRESEND_API_KEYOne key today; prefix anyway for future overrides.
TWITTER_*Twitter / X integrationTWITTER_BEARER_TOKENAlready prefixed. Keep.
CHROME_*CDP browser scraperCHROME_CDP_ENDPOINTAlready prefixed. Keep.
VITE_PUBLIC_*Dashboard build-time, bundled into the browserVITE_PUBLIC_API_URLConvention: any value matching VITE_PUBLIC_* is treated as public by reviewers and CI. Today only VITE_API_URL exists — rename in Phase F #4.
  • API_* — overlaps too much with PLATFORM_* and the existing dashboard API_URL proxy var. Use PLATFORM_API_* instead.
  • DASHBOARD_* — same reason. Use PLATFORM_DASHBOARD_*.
  • SECRET_* / PUBLIC_* — confused with VITE_PUBLIC_*. The prefix-by-domain scheme + the public/secret matrix below handles classification.

Class definitions: see ENV_VARS.md §1.

3.1 🔒 Secret — server-side only, never in /api/config, never in client bundle

Section titled “3.1 🔒 Secret — server-side only, never in /api/config, never in client bundle”
DATABASE_URL
AUTH_SECRET
LLM_CLINE_API_KEY
LLM_ANTHROPIC_API_KEY
LLM_OPENROUTER_API_KEY
NOTION_INTEGRATION_TOKEN
GMAIL_CLIENT_SECRET
GMAIL_REFRESH_TOKEN
TWITTER_BEARER_TOKEN
RESEND_API_KEY

3.2 🪪 Identifier — may appear in /api/config if a client needs it

Section titled “3.2 🪪 Identifier — may appear in /api/config if a client needs it”
NOTION_DATABASE_ID
GMAIL_CLIENT_ID
GMAIL_USER_EMAIL (renamed from MY_EMAIL_ADDRESS)

3.3 🌐 Public config — may appear in client bundle (must be VITE_PUBLIC_* to do so)

Section titled “3.3 🌐 Public config — may appear in client bundle (must be VITE_PUBLIC_* to do so)”
PLATFORM_APP_ENV
PLATFORM_API_URL
PLATFORM_DASHBOARD_URL
AUTH_URL
LLM_PROVIDER
LLM_EXTRACT_URLS_MODEL
LLM_ANALYZE_CONTENT_MODEL
CHROME_CDP_ENDPOINT
GMAIL_SERVICE_ACCOUNT_PATH
VITE_PUBLIC_API_URL

Each row is a separate PR in Phase F. Order minimizes blast radius. Every PR adds a shim that reads the new name first, falls back to the legacy name(s), and logs a [env] DEPRECATED: <old> → <new> warning on fallback.

#DomainLegacy → TargetNotes
1LLMCLINE_API_KEYLLM_CLINE_API_KEYAdd LLM_PROVIDER enum (cline|anthropic|openrouter|auto, default auto).
1LLMANTHROPIC_API_KEYLLM_ANTHROPIC_API_KEY
1LLMOPEN_ROUTER_API_KEYLLM_OPENROUTER_API_KEYNote: drops the underscore between OPEN and ROUTER to match brand.
2AuthBETTER_AUTH_SECRETAUTH_SECRETAUTH_SECRET already exists in schema (unused) — Phase C resolves the dead-field state; Phase F #2 promotes it.
2AuthBETTER_AUTH_URLAUTH_URL
3DBDATABASE_HOST/USER/PASSWORD/NAME/PORTdroppedSchema cleanup. Standardize on DATABASE_URL.
4DashboardVITE_API_URLVITE_PUBLIC_API_URLRequires coordinated rebuild + redeploy — Vite injects at build time, so the rename re-bakes the bundle. Schedule on a deploy window.
5GmailMY_EMAIL_ADDRESSGMAIL_USER_EMAILFolds the outlier into the GMAIL_* family.
6PlatformAPI_URLPLATFORM_API_URL (dashboard + cli usages)apps/cli already uses PLATFORM_API_URL; this aligns dashboard + api.
6PlatformDASHBOARD_URLPLATFORM_DASHBOARD_URL
6PlatformAPP_ENVPLATFORM_APP_ENV

The shim is the only sanctioned way to read env in apps/packages. Sketch (final shape lands in Phase C inside packages/platform-utils/env/):

function readEnv(target: string, ...legacy: string[]): string | undefined {
const v = process.env[target];
if (v !== undefined && v !== '') return v;
for (const name of legacy) {
const fallback = process.env[name];
if (fallback !== undefined && fallback !== '') {
console.warn(`[env] DEPRECATED: ${name} is renamed to ${target}; update your .env`);
return fallback;
}
}
return undefined;
}

Rules:

  • Only the loader (packages/platform-utils/env/) calls process.env directly. Phase I adds an ESLint rule enforcing this.
  • Each shim entry comes with a stated removal release in the PR description. Default: one release per domain before dropping the legacy name.
  • Coolify env UI must be updated to the new name before the legacy fallback is removed.

5. Verification checklist (per Phase F PR)

Section titled “5. Verification checklist (per Phase F PR)”
  • Schema in apps/api/core/env/parseEnvSchema.ts updated.
  • .env.example regenerated (Phase E gate prevents manual drift).
  • Shim added; legacy name still works with a single warning at boot.
  • CI vars/secrets referencing the legacy name renamed (operator action — flag in PR body).
  • Coolify .env updated on staging, smoke-tested, then production (operator action — flag in PR body).
  • Boot log shows the new name in use after redeploy; no DEPRECATED warning.

  • Keep the BETTER_AUTH_* shim forever for better-auth ecosystem compatibility, or drop after one release?
  • Are there Coolify-only env vars (set in the UI, not in docker-compose.coolify.yml) that don’t appear in this map? Someone with Coolify access should export and reconcile before Phase F #1 ships.
  • Should ${env:...} in user-authored presets (a future feature) be restricted to an allowlist? If so, this naming map can double as that allowlist.