Hogsend
Operating

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:

Deploy on Railway

It stands up six services, pre-wired:

ServiceWhat it is
apiHono HTTP server (railway.toml) — REST, ingestion, auth, /v1/health
workerHatchet worker (railway.worker.toml) — durable task execution
PostgresTimescaleDB (Postgres 18) — your primary database
RedisRequired — Better Auth sessions + cross-replica auth rate limiting + reset tokens, PostHog property cache
hatchet-liteSelf-hosted workflow engine (the hatchet-lite image)
Hatchet PostgresHatchet'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:

  1. 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 token registers the account (or logs in if it exists — on a locked-down instance use its seeded ADMIN_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.

  2. An email provider — Hogsend sends email through a swappable provider, Resend by default. For Resend, set RESEND_API_KEY and RESEND_FROM_EMAIL on both the api and worker. For Postmark, set EMAIL_PROVIDER=postmark plus the POSTMARK_* vars instead. RESEND_API_KEY is optional now — a Postmark-only deploy boots without it — so the api no longer fails its /v1/health check 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.

  3. First Studio admin — public sign-up is disabled, so set STUDIO_ADMIN_EMAIL (and optionally STUDIO_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 with railway 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 --json

Expect 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:

ServicePurposeProduction
TimescaleDB (Postgres 18)Primary databaseRailway managed Postgres or a dedicated instance
RedisRequired — Better Auth sessions + reset tokens + cross-replica auth rate limiting, PostHog property cachingRailway managed Redis
Hatchet-LiteWorkflow orchestration engineSelf-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 = 3
  • preDeployCommand runs pnpm db:migrate (engine track first, then your client track) before the new version receives traffic.
  • healthcheckPath is /v1/health with a 120-second timeout; it passes only when both migration tracks are in sync.
  • startCommand boots the Hono server, which runs the engine-track schema guard on boot and exit(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:

VariableValue
DATABASE_URLConnection string to Hatchet's Postgres
SERVER_URLPublic URL of the Hatchet service
SERVER_AUTH_COOKIE_DOMAINDomain for auth cookies
SERVER_DEFAULT_ENGINE_VERSIONV1
SERVER_GRPC_BIND_ADDRESS0.0.0.0
SERVER_GRPC_PORT7077
SERVER_GRPC_BROADCAST_ADDRESSInternal Railway address for gRPC
SERVER_MSGQUEUE_KINDpostgres

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

VariableWhy
STUDIO_ADMIN_EMAILMints 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_EMAILThe 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_URLYour real public api URL — they build the unsubscribe + first-party tracking links inside outgoing email.

DNS and Cloudflare

  1. In Railway, generate or attach a domain for the API service.
  2. In your DNS provider (e.g. Cloudflare), create a CNAME api.yourdomain.com → the Railway domain.
  3. Set API_PUBLIC_URL and BETTER_AUTH_URL to https://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), or EMAIL_PROVIDER=postmark + POSTMARK_*
  • Set STUDIO_ADMIN_EMAIL to mint the first admin (or run railway run hogsend studio admin create)
  • Set NODE_ENV=production, API_PUBLIC_URL, BETTER_AUTH_URL
  • Create two services (railway.toml for API, railway.worker.toml for worker)
  • Widen watch paths so packages/** changes trigger redeploys
  • Confirm preDeployCommand (pnpm db:migrate) ran both tracks
  • Verify /v1/health returns status: "healthy" with both tracks inSync: true

Next steps

On this page