Railway Environment Variables
Scope (2026-06-04): This doc covers env vars for the dev environment (Railway). Staging + prod run on AWS ECS Fargate β see
docs/ops/ecs-prod-deployment-spec.mdfor the ECS env list. The file name is retained for historical link stability; treat it asdev-env-varsfor new reads.
Set these in Railway for the dev environment. Never commit real values to the repo.
Domain Configuration β HWORK_DOMAINβ
HWORK_DOMAIN is the bare email/CORS domain. It controls Specialist email addresses ({firstname}`@`{HWORK_DOMAIN}), transactional email from-address (onboarding@{HWORK_DOMAIN}), and the wildcard CORS allowlist (`*.`{HWORK_DOMAIN}).
| Service | Variable | Dev value | Staging value | Production value |
|---|---|---|---|---|
| API | HWORK_DOMAIN | dev.h852.work | h852.work | h.work |
| Frontend | NEXT_PUBLIC_HWORK_DOMAIN | dev.h852.work | h852.work | h.work |
Replaces:
SPECIALIST_EMAIL_DOMAINandNEXT_PUBLIC_APP_DOMAIN(both deprecated β remove from any env where they appear).
App URL β APP_BASE_URLβ
APP_BASE_URL is the public URL of the user-facing app, used in outbound user-facing replies (WhatsApp pairing prompts, etc.). It is intentionally separate from HWORK_DOMAIN because the app and the email domain are not always the same host:
| Service | Variable | Dev value | Staging value | Production value |
|---|---|---|---|---|
| API | APP_BASE_URL | https://dev.h852.work | https://staging.h852.work | https://app.h.work |
Why: dev runs the entire app on dev.h852.work (no app. prefix), while staging/prod use app.{HWORK_DOMAIN}``. Constructing URLs from HWORK_DOMAIN alone would produce https://app.dev.h852.work for dev, which doesn't exist.
Canonical environment selector β APP_ENVβ
APP_ENV is the canonical environment-name source for all backend services. NODE_ENV is always production in any deployed Node build, so it cannot distinguish staging from production.
| Service | Variable | Acceptable values | Notes |
|---|---|---|---|
API (api/) | APP_ENV | development | staging | production | Read by api/src/common/env.ts (IS_PRODUCTION, IS_STAGING, IS_DEV, IS_DEPLOYED). |
| Frontend (Vercel) | NEXT_PUBLIC_APP_ENV | same values | Frontend mirror β Next.js requires the NEXT_PUBLIC_ prefix to expose env to the browser bundle. Read by frontend/src/lib/env.ts. |
Agent (agent/) | APP_ENV | same values | Same convention. |
Set this per Railway environment (and per Vercel environment) β never in code. The hard-delete-org endpoint and other dev-only paths are gated on IS_PRODUCTION === false.
WhatsApp / Twilioβ
One inbound WhatsApp number per environment. Set WHATSAPP_PHONE_NUMBER to the E.164 number for that env. Leave it unset on environments that don't have a number provisioned yet β the UI will show "not configured" and inbound routing will be inert.
| Variable | Dev | Staging | Production |
|---|---|---|---|
WHATSAPP_PHONE_NUMBER | +18566992888 | (unset β not provisioned) | (unset β not provisioned) |
TWILIO_WEBHOOK_BASE_URL | https://api-dev-b36f.up.railway.app | https://api-staging-bd3f.up.railway.app | https://api-production-f95e.up.railway.app |
TWILIO_ACCOUNT_SID | ACb23672b636fa0d15522099ec5119bb3f (main) | same (main) | same (main) |
TWILIO_API_KEY / TWILIO_API_SECRET | (per env) | (per env) | (per env) |
TWILIO_AUTH_TOKEN | main account auth token (preferred for sig verify) | same | same |
TWILIO_SUBACCOUNT_SID / TWILIO_SUBACCOUNT_TOKEN | optional β fallback for subaccount-owned senders | same | same |
The Twilio webhook for each WhatsApp sender must be set in the Twilio Console (Messaging β Senders) to ${TWILIO_WEBHOOK_BASE_URL}/channels/whatsapp/inbound. The dev sender (+18566992888, SID XE5d4de7ff487f3fd6efc37e970205f8f9) currently points at the dev API.
Legacy data: The
whatsapp_number_pooltable still exists for back-compat with old tests, but routing readsWHATSAPP_PHONE_NUMBERdirectly. New senders are linked to orgs via pairing codes (whatsapp_linked_numbers).
Nango (OAuth Integration Layer)β
Nango is self-hosted on Railway per environment. See docs/integrations/NANGO_SETUP.md for full deployment details.
| Variable | Service | Notes |
|---|---|---|
NANGO_SERVER_URL | API | Internal Railway URL of the Nango server for this env |
NANGO_SECRET_KEY | API | Nango secret key (dev key for dev/staging, prod key for production) |
These are set on the api service in each Railway environment.
Cloudflare R2 (File Storage)β
R2 buckets exist for all three environments: hwork-dev, hwork-staging, hwork.
| Variable | Service | Notes |
|---|---|---|
R2_ACCOUNT_ID | API | Cloudflare account ID |
R2_ACCESS_KEY_ID | API | R2 access key |
R2_SECRET_ACCESS_KEY | API | R2 secret |
R2_BUCKET_NAME | API | hwork-dev / hwork-staging / hwork fallback bucket |
R2_CLIENT_BUCKET | API | Private client-data bucket fallback for attachments/runtime artifacts |
RUNTIME_ARTIFACTS_R2_BUCKET | API | Private runtime artifact bucket used for short-lived signed leases; falls back to R2_CLIENT_BUCKET then R2_BUCKET_NAME |
Currently used for Haystack source document uploads, specialist config storage, uploads, and runtime artifact lease signing. Runtime artifact signed URLs are generated only at response time and must not be persisted.
Cloudflare Email Workerβ
The Cloudflare email worker forwards inbound emails to the API. It uses a shared secret for HMAC validation.
| Variable | Service | Notes |
|---|---|---|
CLOUDFLARE_EMAIL_WEBHOOK_SECRET | API + CF Worker | Shared HMAC secret; set on both sides. Must match. |
Worker routes: @dev.h852.work β dev API, @h852.work β staging API.
Haystack / Hayhooksβ
| Variable | Service | Notes |
|---|---|---|
HAYSTACK_BASE_URL / HAYHOOKS_BASE_URL | API | Railway-internal URL for the Hayhooks service, usually http://humanwork-rag.railway.internal:1416. |
HAYSTACK_API_KEY / HAYHOOKS_API_KEY | API + Hayhooks | Shared API key used by humanwork-api when calling Hayhooks. |
HAYSTACK_TIMEOUT_MS | API | Hayhooks request timeout. Default 15000. |
HAYSTACK_CIRCUIT_THRESHOLD | API | Consecutive per-org Haystack failures before opening the circuit. Default 3. |
HAYSTACK_CIRCUIT_TIMEOUT_MS | API | Milliseconds a per-org Haystack circuit stays open before one half-open probe is allowed. Default 30000. |
HAYSTACK_TOP_K | API | Conversation-context retrieval limit. Falls back to 5. |
HAYSTACK_MAX_UPLOAD_MB | API | Max KB-document upload size in megabytes. Falls back to 25. |
PG_CONN_STR | Hayhooks | Postgres connection string used by Haystack's pgvector backend. |
OPENROUTER_API_KEY | Hayhooks | OpenRouter key for embeddings, reranking, and model calls. |
Agent: AI image generation (#4022) + L3-public web research (#4028)β
Set on the agent service (Railway dev) / ECS agent task environment[] (staging + prod). Both *_ENABLED vars are kill switches with a fail-safe OFF default β the shared agent image bakes them to false, so each environment must opt in explicitly. There is a second, per-tenant gate on the API side (ai_image_generation_enabled / ai_web_research_enabled feature flags, default ON); the agent env is the hard runtime kill and also prevents provider (OpenRouter) spend when off.
| Variable | Dev (Railway) | Staging | Production | Notes |
|---|---|---|---|---|
HW_IMAGE_GEN_ENABLED | true | true | false (until sign-off) | Image-generation kill switch. |
HW_WEB_RESEARCH_ENABLED | true | true | false (until sign-off) | Web-research kill switch. |
HW_IMAGE_GEN_MODEL | google/gemini-3.1-flash-lite-image | same | same | Overridable per directive. |
HW_IMAGE_GEN_BASE_URL | https://openrouter.ai/api/v1 | same | same | OpenRouter Unified Image API base. |
HW_WEB_RESEARCH_MODEL | openai/gpt-4o-mini | same | same | Web-plugin research call. |
HW_WEB_RESEARCH_ROUTER_MODEL | openai/gpt-4o-mini | same | same | Cheap egress-free routing decision. |
Staging + prod are set in code, not by hand: the values above live in
infra/terraform/envs/{staging,prod}/main.tf(hw_image_gen_enabled/hw_web_research_enabledon themodule "stack"call) and the model/base-url defaults ininfra/terraform/modules/stack/variables.tf. They take effect on the nextterraform-apply(staging auto on push tostaging; prod is a manualworkflow_dispatch) followed by an agent redeploy. No manual Secrets Manager step β these are non-sensitive config, not secrets.
Database Pool Sizingβ
The TypeORM pool is env-driven. Sizing depends on the environment's pooler shape β see docs/ops/deploy.md for the full hosting + pooler table.
- dev (Railway): No pooler. API talks direct to Postgres. Decision recorded in #1233 (2026-06-05).
- staging + prod (AWS ECS): AWS RDS Proxy fronts RDS PostgreSQL (Terraform:
infra/terraform/modules/data/main.tf,aws_db_proxy.this). Pool sizing must respect the RDS Proxymax_connections_percentΓ RDS instancemax_connectionsbudget, divided across all ECS tasks.
| Variable | Service | Notes |
|---|---|---|
DATABASE_POOL_MAX | API | Max pool connections per pod. Default 20 (raised from 5 in #2540 β the Ops setup wizard step-3 fans out ~8 parallel API calls; a pool of 5 exhausted under that burst). Deploys behind RDS Proxy (staging/prod) or with tight Railway limits should override via env. |
DATABASE_POOL_IDLE_TIMEOUT_MS | API | Idle connection eviction. Default 10000. |
DATABASE_STATEMENT_TIMEOUT_MS | API | Per-statement timeout pushed via SET statement_timeout. Default 30000. Set to 0 behind RDS Proxy, which rejects this at connect time β staging/prod rely on the DB parameter-group default instead. |
DATABASE_IDLE_TX_TIMEOUT_MS | API | Idle-in-transaction timeout. Same RDS Proxy caveat as above β 0 disables it at connect time. |
When running multi-pod (#865/#1007), DATABASE_POOL_MAX Γ pod_count must stay below the upstream limit β RDS Proxy on AWS, or the raw Postgres max_connections on Railway dev.
Sentry (Error Tracking)β
Sentry is wired into both the NestJS API and the FastAPI agent (#845 closes #828), and into the Next.js frontend.
| Variable | Service | Notes |
|---|---|---|
SENTRY_DSN | API | NestJS DSN for the humanwork-api project. |
SENTRY_DSN | Agent | FastAPI DSN for the humanwork-agent project (separate DSN β distinct project so traces don't cross-pollute). |
NEXT_PUBLIC_SENTRY_DSN | Frontend | Browser DSN. NEXT_PUBLIC_ prefix required to expose to the bundle. |
SENTRY_ENVIRONMENT | API + Agent + Frontend | Mirrors APP_ENV. Used to scope Sentry environments (development/staging/production). |
SENTRY_TRACES_SAMPLE_RATE | API + Agent | Tracing sample rate. Production default 0.1; staging 1.0. |
The Redis-disconnect alert hook for ThrottlerModule shadow mode (see below) routes through these DSNs (#839).
Throttler (Rate Limiting, Shadow Mode)β
ThrottlerModule was re-registered in shadow mode in #812 (closes #803). It logs would-block decisions but does not actually reject requests in production yet. Sentry alerts fire on Redis disconnect (#839).
| Variable | Service | Notes |
|---|---|---|
THROTTLER_ENABLED | API | true to enable shadow-mode evaluation. Default true in staging/prod, false in dev. |
THROTTLER_SHADOW_MODE | API | true (default) β log only, never block. Flip to false only after the shadow-mode burn-in is complete. |
THROTTLER_REDIS_URL | API | Redis URL backing the throttler. Usually REDIS_URL. |
THROTTLER_DEFAULT_TTL_MS | API | Window length, default 60000. |
THROTTLER_DEFAULT_LIMIT | API | Requests per window per key, default 100. |
Lago (Billing)β
Self-hosted Lago drives metered billing for LLM cost rollups and per-Specialist usage (#1069, #1105, #1155 (note: #1165 was closed-unmerged and superseded by #1155)). See docs/decisions/2026-05-29-lago-pricing-strategy.md.
| Variable | Service | Notes |
|---|---|---|
LAGO_API_URL | API | Base URL of the Lago API. Self-hosted Lago β dev points at the Railway-hosted Lago instance; staging + prod point at the AWS-hosted Lago instance (single consolidated instance with separate Lago orgs per env). AWS Lago bring-up tracked in #1287. |
LAGO_API_KEY | API | Lago API key (per environment β separate keys for dev/staging/prod). |
LAGO_WEBHOOK_SECRET | API | HMAC secret for inbound Lago webhooks (invoice, subscription, usage events). |
LAGO_ORG_BILLING_ENABLED | API | Feature flag; true enables Lago wiring for new orgs. Default false until per-env rollout completes. |
LAGO_DEFAULT_PLAN_CODE | API | Lago plan code applied to newly onboarded orgs (e.g. hwork-managed-default). |
LLM cost events are emitted by LlmCostRollupService (see ADR-020 and isolation matrix in CLAUDE.md) and posted to Lago as billable events keyed by osa_id.
Gmail OSA (Send-as Specialist, ADR-0002 Phase 2)β
Phase 2 of the Gmail integration (#1065/#1074/#1076/#1079/#1089/#1093/#1095/#1097) wires send-as-Specialist using a Google Workspace OAuth app plus the Workspace Admin SDK. The decision ("ADR-0002" in PR commit messages) is not yet a standalone doc in docs/decisions/; threat model and acceptance criteria are tracked in the PR descriptions for #1065 and #1076.
| Variable | Service | Notes |
|---|---|---|
GMAIL_OAUTH_CLIENT_ID | API | OAuth client ID for the per-environment Google Cloud project. |
GMAIL_OAUTH_CLIENT_SECRET | API | OAuth client secret. |
GMAIL_OAUTH_REDIRECT_URI | API | Must be allowlisted in the Google Cloud OAuth consent screen. Usually ${APP_BASE_URL}/api/integrations/gmail/oauth/callback. |
GMAIL_WORKSPACE_ADMIN_SUBJECT | API | Workspace super-admin email used for domain-wide delegation when minting send-as identities. |
GMAIL_WORKSPACE_SERVICE_ACCOUNT_JSON | API | JSON key for the service account that holds domain-wide delegation. Base64-encoded in Railway. |
GMAIL_OSA_ENABLED | API | Feature flag β true enables the Phase 2 send-as flow. Default false until per-org rollout. |
Domain-wide delegation must be granted in the Google Workspace Admin Console with the full WORKSPACE_DWD_SCOPES set from api/src/workspace-lifecycle/workspace-lifecycle.constants.ts β minimum-necessary 9 scopes: admin.directory.user, admin.directory.user.security, gmail.modify, gmail.settings.basic, calendar, drive, contacts, meetings.space.created, meetings.space.readonly. Broader Google scopes subsume narrower ones (e.g. gmail.modify covers gmail.send/readonly/metadata, calendar covers calendar.events, drive covers drive.file), so only the broad scope is granted. Meet's two scopes are independent β meetings.space.created covers spaces the SA created, meetings.space.readonly reads any of the Specialist's Meet conferences. See architecture/workspace-lifecycle.md Β§Scopes for full subsumption notes (verified against Google's OAuth scopes master index 2026-06-08). Scopes added to the constants list MUST be granted on the DWD client at the same time β DWD matches by exact string, drift fails closed with unauthorized_client.
Google Workspace Admin SDK (GSuite user provisioning)β
The Admin SDK client (api/src/workspace-lifecycle/google-admin.client.ts) provisions per-Specialist workspace users. It reads service account credentials from environment variables.
| Variable | Service | Notes |
|---|---|---|
GSUITE_SERVICE_ACCOUNT_KEY_JSON | API | (Canonical) Full service account JSON string for the DWD-enabled service account. |
GSUITE_ADMIN_USER_EMAIL | API | (Canonical) Workspace super-admin email to impersonate via Domain-Wide Delegation. |
GOOGLE_SERVICE_ACCOUNT_JSON | API | (Deprecated alias) Falls back to this if GSUITE_SERVICE_ACCOUNT_KEY_JSON is unset. Logs a deprecation warning at startup. |
GOOGLE_ADMIN_EMAIL | API | (Deprecated alias) Falls back to this if GSUITE_ADMIN_USER_EMAIL is unset. Logs a deprecation warning at startup. |
Migration: If your environment uses the GOOGLE_* names, either:
- Add the canonical
GSUITE_*variables as variable references pointing to the existingGOOGLE_*values, OR - Duplicate the secret values into the new
GSUITE_*variable names.
The canonical names take precedence when both are set. The deprecated aliases exist for Railway dev environment compatibility (naming drift from Terraform).