Skip to main content

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.md for the ECS env list. The file name is retained for historical link stability; treat it as dev-env-vars for 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}).

ServiceVariableDev valueStaging valueProduction value
APIHWORK_DOMAINdev.h852.workh852.workh.work
FrontendNEXT_PUBLIC_HWORK_DOMAINdev.h852.workh852.workh.work

Replaces: SPECIALIST_EMAIL_DOMAIN and NEXT_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:

ServiceVariableDev valueStaging valueProduction value
APIAPP_BASE_URLhttps://dev.h852.workhttps://staging.h852.workhttps://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.

ServiceVariableAcceptable valuesNotes
API (api/)APP_ENVdevelopment | staging | productionRead by api/src/common/env.ts (IS_PRODUCTION, IS_STAGING, IS_DEV, IS_DEPLOYED).
Frontend (Vercel)NEXT_PUBLIC_APP_ENVsame valuesFrontend 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_ENVsame valuesSame 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.

VariableDevStagingProduction
WHATSAPP_PHONE_NUMBER+18566992888(unset β€” not provisioned)(unset β€” not provisioned)
TWILIO_WEBHOOK_BASE_URLhttps://api-dev-b36f.up.railway.apphttps://api-staging-bd3f.up.railway.apphttps://api-production-f95e.up.railway.app
TWILIO_ACCOUNT_SIDACb23672b636fa0d15522099ec5119bb3f (main)same (main)same (main)
TWILIO_API_KEY / TWILIO_API_SECRET(per env)(per env)(per env)
TWILIO_AUTH_TOKENmain account auth token (preferred for sig verify)samesame
TWILIO_SUBACCOUNT_SID / TWILIO_SUBACCOUNT_TOKENoptional β€” fallback for subaccount-owned senderssamesame

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_pool table still exists for back-compat with old tests, but routing reads WHATSAPP_PHONE_NUMBER directly. 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.

VariableServiceNotes
NANGO_SERVER_URLAPIInternal Railway URL of the Nango server for this env
NANGO_SECRET_KEYAPINango 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.

VariableServiceNotes
R2_ACCOUNT_IDAPICloudflare account ID
R2_ACCESS_KEY_IDAPIR2 access key
R2_SECRET_ACCESS_KEYAPIR2 secret
R2_BUCKET_NAMEAPIhwork-dev / hwork-staging / hwork fallback bucket
R2_CLIENT_BUCKETAPIPrivate client-data bucket fallback for attachments/runtime artifacts
RUNTIME_ARTIFACTS_R2_BUCKETAPIPrivate 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.

VariableServiceNotes
CLOUDFLARE_EMAIL_WEBHOOK_SECRETAPI + CF WorkerShared HMAC secret; set on both sides. Must match.

Worker routes: @dev.h852.work β†’ dev API, @h852.work β†’ staging API.


Haystack / Hayhooks​

VariableServiceNotes
HAYSTACK_BASE_URL / HAYHOOKS_BASE_URLAPIRailway-internal URL for the Hayhooks service, usually http://humanwork-rag.railway.internal:1416.
HAYSTACK_API_KEY / HAYHOOKS_API_KEYAPI + HayhooksShared API key used by humanwork-api when calling Hayhooks.
HAYSTACK_TIMEOUT_MSAPIHayhooks request timeout. Default 15000.
HAYSTACK_CIRCUIT_THRESHOLDAPIConsecutive per-org Haystack failures before opening the circuit. Default 3.
HAYSTACK_CIRCUIT_TIMEOUT_MSAPIMilliseconds a per-org Haystack circuit stays open before one half-open probe is allowed. Default 30000.
HAYSTACK_TOP_KAPIConversation-context retrieval limit. Falls back to 5.
HAYSTACK_MAX_UPLOAD_MBAPIMax KB-document upload size in megabytes. Falls back to 25.
PG_CONN_STRHayhooksPostgres connection string used by Haystack's pgvector backend.
OPENROUTER_API_KEYHayhooksOpenRouter 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.

VariableDev (Railway)StagingProductionNotes
HW_IMAGE_GEN_ENABLEDtruetruefalse (until sign-off)Image-generation kill switch.
HW_WEB_RESEARCH_ENABLEDtruetruefalse (until sign-off)Web-research kill switch.
HW_IMAGE_GEN_MODELgoogle/gemini-3.1-flash-lite-imagesamesameOverridable per directive.
HW_IMAGE_GEN_BASE_URLhttps://openrouter.ai/api/v1samesameOpenRouter Unified Image API base.
HW_WEB_RESEARCH_MODELopenai/gpt-4o-minisamesameWeb-plugin research call.
HW_WEB_RESEARCH_ROUTER_MODELopenai/gpt-4o-minisamesameCheap 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_enabled on the module "stack" call) and the model/base-url defaults in infra/terraform/modules/stack/variables.tf. They take effect on the next terraform-apply (staging auto on push to staging; prod is a manual workflow_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 Proxy max_connections_percent Γ— RDS instance max_connections budget, divided across all ECS tasks.
VariableServiceNotes
DATABASE_POOL_MAXAPIMax 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_MSAPIIdle connection eviction. Default 10000.
DATABASE_STATEMENT_TIMEOUT_MSAPIPer-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_MSAPIIdle-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.

VariableServiceNotes
SENTRY_DSNAPINestJS DSN for the humanwork-api project.
SENTRY_DSNAgentFastAPI DSN for the humanwork-agent project (separate DSN β€” distinct project so traces don't cross-pollute).
NEXT_PUBLIC_SENTRY_DSNFrontendBrowser DSN. NEXT_PUBLIC_ prefix required to expose to the bundle.
SENTRY_ENVIRONMENTAPI + Agent + FrontendMirrors APP_ENV. Used to scope Sentry environments (development/staging/production).
SENTRY_TRACES_SAMPLE_RATEAPI + AgentTracing 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).

VariableServiceNotes
THROTTLER_ENABLEDAPItrue to enable shadow-mode evaluation. Default true in staging/prod, false in dev.
THROTTLER_SHADOW_MODEAPItrue (default) β†’ log only, never block. Flip to false only after the shadow-mode burn-in is complete.
THROTTLER_REDIS_URLAPIRedis URL backing the throttler. Usually REDIS_URL.
THROTTLER_DEFAULT_TTL_MSAPIWindow length, default 60000.
THROTTLER_DEFAULT_LIMITAPIRequests 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.

VariableServiceNotes
LAGO_API_URLAPIBase 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_KEYAPILago API key (per environment β€” separate keys for dev/staging/prod).
LAGO_WEBHOOK_SECRETAPIHMAC secret for inbound Lago webhooks (invoice, subscription, usage events).
LAGO_ORG_BILLING_ENABLEDAPIFeature flag; true enables Lago wiring for new orgs. Default false until per-env rollout completes.
LAGO_DEFAULT_PLAN_CODEAPILago 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.

VariableServiceNotes
GMAIL_OAUTH_CLIENT_IDAPIOAuth client ID for the per-environment Google Cloud project.
GMAIL_OAUTH_CLIENT_SECRETAPIOAuth client secret.
GMAIL_OAUTH_REDIRECT_URIAPIMust be allowlisted in the Google Cloud OAuth consent screen. Usually ${APP_BASE_URL}/api/integrations/gmail/oauth/callback.
GMAIL_WORKSPACE_ADMIN_SUBJECTAPIWorkspace super-admin email used for domain-wide delegation when minting send-as identities.
GMAIL_WORKSPACE_SERVICE_ACCOUNT_JSONAPIJSON key for the service account that holds domain-wide delegation. Base64-encoded in Railway.
GMAIL_OSA_ENABLEDAPIFeature 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.

VariableServiceNotes
GSUITE_SERVICE_ACCOUNT_KEY_JSONAPI(Canonical) Full service account JSON string for the DWD-enabled service account.
GSUITE_ADMIN_USER_EMAILAPI(Canonical) Workspace super-admin email to impersonate via Domain-Wide Delegation.
GOOGLE_SERVICE_ACCOUNT_JSONAPI(Deprecated alias) Falls back to this if GSUITE_SERVICE_ACCOUNT_KEY_JSON is unset. Logs a deprecation warning at startup.
GOOGLE_ADMIN_EMAILAPI(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:

  1. Add the canonical GSUITE_* variables as variable references pointing to the existing GOOGLE_* values, OR
  2. 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).