Skip to main content

Environment variables

This page lists every environment variable that Better Comply reads, grouped by concern. Pull these directly from packages/backend/src/config.ts (backend) and VITE_BACKEND_URL (web).

Who this is for

Operators configuring a deployment. On Google Cloud Run, secrets are stored in Secret Manager and mounted as environment variables at deploy time - do not bake secrets into container images.

warning

Never log or commit secret values. Example values on this page are obviously fake. Treat SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY, GEMINI_API_KEY, BOOTSTRAP_CODE, RECERTIFICATION_SCAN_SECRET, SUPERVISOR_REPORT_SCAN_SECRET, and EMAIL_API_KEY as credentials.

Web (frontend) variables

These are set at build time in the Netlify environment or in .env for local development. They are embedded in the static bundle, so never put secrets here.

NameRequiredPurpose
VITE_BACKEND_URLYesBase URL of the Cloud Run backend, e.g. https://api.better-comply.example. The frontend helper throws at the call site if this is unset. No trailing slash.

Backend variables

These are read by packages/backend/src/config.ts at startup using Zod validation. The process will not start if a required variable is missing or invalid.

Core runtime

NameRequiredDefaultPurpose
NODE_ENVNodevelopmentNode environment. Set to production on Cloud Run. Also used (with ENVIRONMENT) to determine isProduction.
ENVIRONMENTNodevelopmentDeployment environment string. production or prod enables production guards (e.g. demo seeding refused, scan secrets required).
PORTNo8080Port the Fastify server listens on. Cloud Run sets this automatically.
LOG_LEVELNoinfoPino log level: fatal, error, warn, info, debug, trace, or silent.

Supabase connection

NameRequiredPurpose
SUPABASE_URLYesYour Supabase project URL, e.g. https://abcdefghij.supabase.co.
SUPABASE_SERVICE_ROLE_KEYYesService-role JWT. Used for administrative writes that bypass RLS. Keep secret.
SUPABASE_ANON_KEYYesAnonymous public key. Used when constructing caller-scoped clients.

AI providers

At least one AI provider key is needed for content generation, quiz generation, and RAG retrieval features.

NameRequiredPurpose
OPENAI_API_KEYNoOpenAI API key. When set, the backend uses the OpenAI provider.
GEMINI_API_KEYNoGoogle Gemini API key. Alternate AI provider.

If neither key is set, AI routes will return errors but the rest of the application remains functional.

CORS

NameRequiredDefaultPurpose
ALLOWED_ORIGINYes (production)* (local dev only)Comma-separated list of allowed CORS origins. Set to the exact production frontend URL, e.g. https://app.better-comply.example. An empty value in production means all origins are accepted - do not leave this unset in production.

Demo seeding

Non-production only

Demo seeding is double-gated and refuses to run when ENVIRONMENT=production or NODE_ENV=production. These variables should not be set in production environments.

NameRequiredDefaultPurpose
BOOTSTRAP_CODENo(unset)The secret access code that must be submitted to the /v1/seed-database endpoint. Compared with a constant-time equality check.
ALLOW_DEMO_SEEDINGNofalseMust be the string true to enable the seeding endpoint. Both this AND BOOTSTRAP_CODE must be satisfied; the endpoint also refuses in production.

See Demo and seeding for full details.

Scheduled jobs - scan secrets

These secrets gate the cron/scan endpoints. All scan endpoints are not user-authenticated - they use the x-internal-secret header and refuse to run in production without the secret. See Scheduled jobs.

NameRequiredPurpose
RECERTIFICATION_SCAN_SECRETYes (production)Shared secret for POST /v1/recertification-scan, POST /v1/process-document-queue, POST /v1/process-document-task, and POST /v1/cleanup-document-conversions. Pass as the x-internal-secret request header. In production the endpoint refuses if this is unset.
SUPERVISOR_REPORT_SCAN_SECRETYes (production)Shared secret for POST /v1/supervisor-report-scan. Same header convention. Separate from the recertification secret so they can be rotated independently.

Document processing

NameRequiredDefaultPurpose
DOCUMENT_PROCESSING_MODENoinlineHow RAG indexing work is triggered. Options: inline, worker, cloud-tasks. See Document processing.
ENABLE_INPROCESS_DOC_WORKERNofalseWhen true and DOCUMENT_PROCESSING_MODE=worker, drains the document queue on an in-process timer. Use only on single-instance Docker deployments, not on multi-instance Cloud Run.
DOC_WORKER_POLL_MSNo30000Interval in milliseconds for the in-process worker. Min 2 000, max 600 000.
DOC_WORKER_BATCH_SIZENo3Number of documents to process per drain cycle. Min 1, max 20.
DOC_STALE_PROCESSING_MSNo300000How long a document can be in processing state before being reclaimed as stale (default 5 minutes). Min 60 000, max 3 600 000.

Cloud Tasks (GCP only)

These are only needed when DOCUMENT_PROCESSING_MODE=cloud-tasks.

NameRequiredPurpose
GCP_PROJECT_IDWhen using Cloud TasksGCP project ID, e.g. my-project-123.
CLOUD_TASKS_LOCATIONWhen using Cloud TasksCloud Tasks region, e.g. us-central1.
CLOUD_TASKS_QUEUEWhen using Cloud TasksName of the Cloud Tasks queue, e.g. doc-index.
CLOUD_TASKS_TARGET_URLWhen using Cloud TasksBase URL of this Cloud Run service. Tasks are delivered to ${CLOUD_TASKS_TARGET_URL}/v1/process-document-task. No trailing slash.

The Cloud Tasks API is reached using the Cloud Run instance's metadata-server token - no extra SDK or credential file is needed.

Email delivery

NameRequiredDefaultPurpose
EMAIL_PROVIDERNoAuto-detectedresend or console. If unset, the backend uses resend when EMAIL_API_KEY is present, and falls back to console (log-only, no real send) when it is not. A missing key is never a hard failure.
EMAIL_API_KEYWhen sending real email(unset)Resend API key, e.g. re_xxxxxxxxxxxxxxxxxxxx.
EMAIL_FROM_ADDRESSNono-reply@better-comply.exampleThe From address for outbound email.
EMAIL_FROM_NAMENoBetter ComplyThe display name in the From field.
APP_BASE_URLNo(unset)Base URL of the web application, e.g. https://app.better-comply.example. Used in email links (e.g. "View your team" in the supervisor digest).

See Email delivery for details on configuring Resend.

Secret rotation

When rotating a secret (e.g. RECERTIFICATION_SCAN_SECRET):

  1. Add the new value as a new version in Secret Manager.
  2. Redeploy the Cloud Run service so the updated secret is mounted.
  3. Update the Cloud Scheduler job header to use the new value.
  4. Verify the first scheduled run succeeds before removing the old Secret Manager version.

Local development example

# packages/backend/.env (never commit this file)

NODE_ENV=development
ENVIRONMENT=development
PORT=8080
LOG_LEVEL=debug

SUPABASE_URL=http://127.0.0.1:54521
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.FAKE_LOCAL_SERVICE_ROLE
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.FAKE_LOCAL_ANON

OPENAI_API_KEY=sk-fake-key-for-local-dev

ALLOWED_ORIGIN=http://localhost:5173

BOOTSTRAP_CODE=local-dev-code
ALLOW_DEMO_SEEDING=true

RECERTIFICATION_SCAN_SECRET=local-dev-recert-secret
SUPERVISOR_REPORT_SCAN_SECRET=local-dev-report-secret

DOCUMENT_PROCESSING_MODE=inline

EMAIL_PROVIDER=console
# packages/web/.env (never commit this file)

VITE_BACKEND_URL=http://localhost:8080