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).
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.
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.
| Name | Required | Purpose |
|---|---|---|
VITE_BACKEND_URL | Yes | Base 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
| Name | Required | Default | Purpose |
|---|---|---|---|
NODE_ENV | No | development | Node environment. Set to production on Cloud Run. Also used (with ENVIRONMENT) to determine isProduction. |
ENVIRONMENT | No | development | Deployment environment string. production or prod enables production guards (e.g. demo seeding refused, scan secrets required). |
PORT | No | 8080 | Port the Fastify server listens on. Cloud Run sets this automatically. |
LOG_LEVEL | No | info | Pino log level: fatal, error, warn, info, debug, trace, or silent. |
Supabase connection
| Name | Required | Purpose |
|---|---|---|
SUPABASE_URL | Yes | Your Supabase project URL, e.g. https://abcdefghij.supabase.co. |
SUPABASE_SERVICE_ROLE_KEY | Yes | Service-role JWT. Used for administrative writes that bypass RLS. Keep secret. |
SUPABASE_ANON_KEY | Yes | Anonymous 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.
| Name | Required | Purpose |
|---|---|---|
OPENAI_API_KEY | No | OpenAI API key. When set, the backend uses the OpenAI provider. |
GEMINI_API_KEY | No | Google 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
| Name | Required | Default | Purpose |
|---|---|---|---|
ALLOWED_ORIGIN | Yes (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
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.
| Name | Required | Default | Purpose |
|---|---|---|---|
BOOTSTRAP_CODE | No | (unset) | The secret access code that must be submitted to the /v1/seed-database endpoint. Compared with a constant-time equality check. |
ALLOW_DEMO_SEEDING | No | false | Must 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.
| Name | Required | Purpose |
|---|---|---|
RECERTIFICATION_SCAN_SECRET | Yes (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_SECRET | Yes (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
| Name | Required | Default | Purpose |
|---|---|---|---|
DOCUMENT_PROCESSING_MODE | No | inline | How RAG indexing work is triggered. Options: inline, worker, cloud-tasks. See Document processing. |
ENABLE_INPROCESS_DOC_WORKER | No | false | When 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_MS | No | 30000 | Interval in milliseconds for the in-process worker. Min 2 000, max 600 000. |
DOC_WORKER_BATCH_SIZE | No | 3 | Number of documents to process per drain cycle. Min 1, max 20. |
DOC_STALE_PROCESSING_MS | No | 300000 | How 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.
| Name | Required | Purpose |
|---|---|---|
GCP_PROJECT_ID | When using Cloud Tasks | GCP project ID, e.g. my-project-123. |
CLOUD_TASKS_LOCATION | When using Cloud Tasks | Cloud Tasks region, e.g. us-central1. |
CLOUD_TASKS_QUEUE | When using Cloud Tasks | Name of the Cloud Tasks queue, e.g. doc-index. |
CLOUD_TASKS_TARGET_URL | When using Cloud Tasks | Base 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
| Name | Required | Default | Purpose |
|---|---|---|---|
EMAIL_PROVIDER | No | Auto-detected | resend 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_KEY | When sending real email | (unset) | Resend API key, e.g. re_xxxxxxxxxxxxxxxxxxxx. |
EMAIL_FROM_ADDRESS | No | no-reply@better-comply.example | The From address for outbound email. |
EMAIL_FROM_NAME | No | Better Comply | The display name in the From field. |
APP_BASE_URL | No | (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):
- Add the new value as a new version in Secret Manager.
- Redeploy the Cloud Run service so the updated secret is mounted.
- Update the Cloud Scheduler job header to use the new value.
- 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
Related
- Scheduled jobs - how secrets are passed to cron endpoints
- Email delivery - configuring Resend
- Document processing -
DOCUMENT_PROCESSING_MODEoptions - Demo and seeding - demo double-gate details