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 (non-session product storage only)​

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_BUCKETAPILegacy/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.

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.

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 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.

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).