Deploy on Railway (one paved option)
Railway is one paved on-ramp for Hogsend — managed Postgres + Redis, self-hosted hatchet-lite, GitHub auto-deploy. Two services from one repo, same build, different entry points.
Railway is one paved option, not the only way — the default deploy path is Docker self-host. Railway trades some control for a managed on-ramp: managed Postgres and Redis, a one-click template, and GitHub auto-deploy. It consumes the same build and the same env contract as every other target.
Deploy with the template (one-click)
The published template provisions the entire topology in one click:
It stands up six services, pre-wired:
| Service | What it is |
|---|---|
| api | Hono HTTP server (railway.toml) — REST, ingestion, auth, /v1/health |
| worker | Hatchet worker (railway.worker.toml) — durable task execution |
| Postgres | TimescaleDB (Postgres 18) — your primary database |
| Redis | Required — Better Auth sessions + cross-replica auth rate limiting + reset tokens, PostHog property cache |
| hatchet-lite | Self-hosted workflow engine (the hatchet-lite image) |
| Hatchet Postgres | Hatchet's own metadata database |
The template wires everything it can on its own: DATABASE_URL and REDIS_URL (service references), the Hatchet gRPC address, a freshly generated BETTER_AUTH_SECRET, and API_PUBLIC_URL / BETTER_AUTH_URL derived from the api's Railway domain. A couple of things still need you after the first deploy:
-
Hatchet token (one-time) — Hatchet mints its client token after the server boots, so it can't be pre-filled. Mint it headlessly with the CLI and pipe it straight into the api service (the worker references the api's value):
railway variables --service hogsend-api --set \ "HATCHET_CLIENT_TOKEN=$(hogsend hatchet token \ --url https://<hatchet-lite-public-url> \ --email you@yourdomain.com --password '<password>')"hogsend hatchet tokenregisters the account (or logs in if it exists — on a locked-down instance use its seededADMIN_EMAIL/ADMIN_PASSWORD), ensures the tenant, creates the token, and prints only the token to stdout. Prefer the manual route? The hatchet-lite dashboard → Settings → API Tokens → Create works too. Then redeploy. See Get a Hatchet token. -
An email provider — Hogsend sends email through a swappable provider, Resend by default. For Resend, set
RESEND_API_KEYandRESEND_FROM_EMAILon both the api and worker. For Postmark, setEMAIL_PROVIDER=postmarkplus thePOSTMARK_*vars instead.RESEND_API_KEYis optional now — a Postmark-only deploy boots without it — so the api no longer fails its/v1/healthcheck just because no Resend key is set. Set the active provider's credentials before sending real email. Starting from a brand-new domain? Production email on a fresh domain covers the full DNS-to-first-send path. -
First Studio admin — public sign-up is disabled, so set
STUDIO_ADMIN_EMAIL(and optionallySTUDIO_ADMIN_PASSWORD) on the api service and the API mints the first admin on boot into an empty user table. Or mint it from a shell withrailway run hogsend studio admin create. See Authentication.
Bringing a custom domain? Attach it to the api service and update API_PUBLIC_URL + BETTER_AUTH_URL to match — those build the unsubscribe + tracking links inside outgoing email, so they must be your real public URL.
Then verify the live api:
hogsend doctor --url https://api.yourapp.com --jsonExpect an ok verdict with database/redis up and both schema tracks in sync.
Everything below is the manual path — wire the same topology up yourself, or deploy into an existing Railway project. It also doubles as a reference for what the template configures.
What you deploy
From your scaffolded repo you run two Railway services that share the same build output but use different entry points:
- API service — Hono HTTP server (
node dist/index.js) handling REST endpoints, ingestion, and auth. - Worker service — Hatchet worker (
node dist/worker.js) executing durable tasks.
Both scale independently. The engine is bundled into your build at compile time (tsup noExternal: ["@hogsend/*"]), so there is no separate engine deploy — upgrading is pnpm up "@hogsend/*", never a git merge.
Supporting infrastructure:
| Service | Purpose | Production |
|---|---|---|
| TimescaleDB (Postgres 18) | Primary database | Railway managed Postgres or a dedicated instance |
| Redis | Required — Better Auth sessions + reset tokens + cross-replica auth rate limiting, PostHog property caching | Railway managed Redis |
| Hatchet-Lite | Workflow orchestration engine | Self-hosted on Railway (Docker image) |
Railway configuration
The scaffold ships both Railway config files at your repo root. They build your app with its own pnpm build (tsup).
API service (railway.toml)
The API service handles HTTP traffic and runs migrations before each deploy:
[build]
buildCommand = "pnpm build"
watchPatterns = ["src/**", "migrations/**", "package.json", "pnpm-lock.yaml", "railway.toml"]
[deploy]
# Two-track migrate: engine track first, then this repo's client track.
preDeployCommand = "pnpm db:migrate"
startCommand = "pnpm start"
healthcheckPath = "/v1/health"
healthcheckTimeout = 120
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3preDeployCommandrunspnpm db:migrate(engine track first, then your client track) before the new version receives traffic.healthcheckPathis/v1/healthwith a 120-second timeout; it passes only when both migration tracks are in sync.startCommandboots the Hono server, which runs the engine-track schema guard on boot andexit(1)s if the engine schema is behind.
Worker service (railway.worker.toml)
The worker runs Hatchet task execution with no HTTP port:
[build]
buildCommand = "pnpm build"
watchPatterns = ["src/**", "package.json", "pnpm-lock.yaml", "railway.worker.toml"]
[deploy]
# No healthcheck — the worker has no HTTP port. Migrations are owned by the API
# service's preDeployCommand; the worker just executes Hatchet tasks.
startCommand = "pnpm worker"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 5- No health check — the worker has no HTTP server; it relies on the restart policy.
- No
preDeployCommand— migrations are owned by the API service. Running them in both would race on the same database.
Two services, one repo
Both services deploy from the same repository. In Railway, create two services pointing to the same repo and assign each its config file (railway.toml for API, railway.worker.toml for worker). They share the same Railway environment variables.
Monorepo watch paths are a live landmine. Railway redeploys a service only when files under its watchPatterns change. In this monorepo, the engine and all shared code live under packages/** — if a service is scoped to redeploy only on apps/api/**, most code changes silently never ship. Set each service's watch path to the repo root (or apps/api/** plus packages/**), and verify after setup that a packages/-only change actually triggers a redeploy. A scaffolded app sidesteps this because its watchPatterns point at its own src/** — but the dogfood monorepo and any repo that vendors packages/ must widen them.
Infrastructure on Railway
Hatchet-Lite
Deploy as a separate Railway service from the Docker image ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest, with its own Postgres instance:
| Variable | Value |
|---|---|
DATABASE_URL | Connection string to Hatchet's Postgres |
SERVER_URL | Public URL of the Hatchet service |
SERVER_AUTH_COOKIE_DOMAIN | Domain for auth cookies |
SERVER_DEFAULT_ENGINE_VERSION | V1 |
SERVER_GRPC_BIND_ADDRESS | 0.0.0.0 |
SERVER_GRPC_PORT | 7077 |
SERVER_GRPC_BROADCAST_ADDRESS | Internal Railway address for gRPC |
SERVER_MSGQUEUE_KIND | postgres |
Once it is running, mint an API token — either headlessly with hogsend hatchet token --url <hatchet-url> --email <e> --password <p> (prints only the token, pipeable) or from the dashboard — and set it as HATCHET_CLIENT_TOKEN in both Hogsend services. See Get a Hatchet token.
Lock down a public hatchet-lite. It ships with open registration — anyone who finds the public URL can create an account on your Hatchet dashboard. Set SERVER_ALLOW_SIGNUP=false, and give it a real seeded admin via ADMIN_EMAIL / ADMIN_PASSWORD (never leave the admin@example.com / Admin123!! defaults). hogsend hatchet token then logs in with those credentials.
Postgres & Redis
Railway's managed Postgres works out of the box; it provides DATABASE_URL automatically when you provision and link it. Redis is required in production — Better Auth's secondaryStorage (sessions, reset tokens, and cross-replica auth rate limiting) gates on a raw REDIS_URL, and it also backs the PostHog property cache. Provision Redis and link it to both services — REDIS_URL is set automatically. For internal Railway networking set HATCHET_CLIENT_TLS_STRATEGY=none.
Environment variables
The three required vars are DATABASE_URL, BETTER_AUTH_SECRET, and HATCHET_CLIENT_TOKEN (plus the other HATCHET_CLIENT_* connection values). Set them in your Railway project environment, shared across both services. Set NODE_ENV=production to disable /docs and /openapi.json.
A few more you'll typically set on Railway (see the configuration reference for the full contract):
| Variable | Why |
|---|---|
STUDIO_ADMIN_EMAIL | Mints the first Studio admin on boot into an empty user table (public sign-up is disabled). Add STUDIO_ADMIN_PASSWORD to set it verbatim, else one is auto-generated + printed once. |
RESEND_API_KEY + RESEND_FROM_EMAIL | The default Resend provider. Optional — a Postmark-only deploy skips these. |
EMAIL_PROVIDER + POSTMARK_* | Optional — set EMAIL_PROVIDER=postmark plus POSTMARK_SERVER_TOKEN (and POSTMARK_WEBHOOK_USER/POSTMARK_WEBHOOK_PASS) to run Postmark instead of Resend. |
API_PUBLIC_URL + BETTER_AUTH_URL | Your real public api URL — they build the unsubscribe + first-party tracking links inside outgoing email. |
DNS and Cloudflare
- In Railway, generate or attach a domain for the API service.
- In your DNS provider (e.g. Cloudflare), create a CNAME
api.yourdomain.com→ the Railway domain. - Set
API_PUBLIC_URLandBETTER_AUTH_URLtohttps://api.yourdomain.com.
The worker needs no public domain.
Deployment checklist
- Get a Hatchet token and deploy Hatchet-Lite
- Provision Postgres and Redis (Redis is required), link them to both services
- Set required env (
DATABASE_URL,BETTER_AUTH_SECRET,HATCHET_CLIENT_TOKEN,HATCHET_CLIENT_*) - Set an email provider —
RESEND_API_KEY+RESEND_FROM_EMAIL(default), orEMAIL_PROVIDER=postmark+POSTMARK_* - Set
STUDIO_ADMIN_EMAILto mint the first admin (or runrailway run hogsend studio admin create) - Set
NODE_ENV=production,API_PUBLIC_URL,BETTER_AUTH_URL - Create two services (
railway.tomlfor API,railway.worker.tomlfor worker) - Widen watch paths so
packages/**changes trigger redeploys - Confirm
preDeployCommand(pnpm db:migrate) ran both tracks - Verify
/v1/healthreturnsstatus: "healthy"with both tracksinSync: true
Next steps
Deploy with Docker (default)
Self-host the full Hogsend stack on any box with docker-compose.prod.yml — Postgres, Redis, hatchet-lite, plus the migrate/api/worker run modes off one image.
Bring your own orchestrator
Run Hogsend on Kubernetes, Nomad, ECS, or any platform you already operate. One image, three run modes, one env contract — you provide Postgres, Redis, and a Hatchet endpoint.