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 (non-session product storage only)β
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 | Legacy/non-session private product storage; never a managed Hermes attachment/output fallback |
R2 may be used by explicitly non-session products such as Haystack source
ingestion. Managed Hermes attachments, generated outputs, skills, memory, and
workspace files live only in the organization's cloud AgentFS, under
authorization-scoped shared or conversation trees, and are addressed by
canonical file:///... locators. Do not configure an R2 bucket as a managed
runtime artifact target, copy AgentFS outputs into it, or persist signed URLs in
SessionDB. See ADR-046.
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. |
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 under the ADR-020 isolation matrix 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).