Skip to content

Hire & LiteLLM Deployment

apps/hire deploys to Railway the same way ops and pool do — GitHub Actions pushes variables with railway variables set, then runs railway up. The one new piece is litellm, a container service rather than a workspace app, which every model call in hire routes through.

Two reusable workflows do the work, both called from deploy-staging.yml and deploy-production.yml:

WorkflowRailway serviceBuilder
.github/workflows/litellm-deploy.ymllitellmDockerfile (infra/litellm/Dockerfile)
.github/workflows/hire-deploy.ymlhireRailpack (pnpm workspace)

hire is deployed after litellm in both orchestrators, since it is useless without a reachable proxy. Neither depends on api.

Deploying a branch to staging before merge

Section titled “Deploying a branch to staging before merge”

deploy-staging.yml takes a services input:

  • all (default) — every service, current behaviour.
  • hire-and-litellm — only these two, leaving the rest of staging untouched.

Run it from Actions → deploy-staging → Run workflow, selecting your branch. workflow_dispatch resolves the workflow file and the reusable workflows it calls from the ref you pick, so a branch that changes either deploy workflow is exercised as written before it merges.

None of this is created by CI — it has to exist in the Railway project first. Everything is per-environment (staging and prod).

  1. Create two services, named exactly hire and litellm. CI addresses them by name (--service hire), and the Railway project itself is resolved from RAILWAY_TOKEN.
  2. litellm needs its own Postgres database. It stores virtual keys, teams and spend logs under its own schema, so give it a database separate from the platform one — either a dedicated Railway Postgres service, or a litellm database inside the existing one, mirroring how docker-compose.yml reuses the shared local Postgres. Put the resulting URL in LITELLM_DATABASE_URL.
  3. litellm needs Redis for its response cache (LITELLM_REDIS_URL).
  4. hire needs its own Postgres database (HIRE_POSTGRES_URL). Its schema is Drizzle-managed and unrelated to the Prisma schema in packages/database; migrations run automatically at container start (see below). Redis is optional (HIRE_REDIS_URL) — without it, rate limiting and resumable streams are skipped, nothing breaks.
  5. Create the users team in the LiteLLM dashboard. apps/hire/lib/litellm.ts looks up a team whose alias is literally users and provisions a per-user virtual key against it. If that team is missing, key generation fails silently and every chat request is rejected with bad_request:chat. Grant it access to at least the default model alias.
  6. Enable Magic Auth in the WorkOS dashboard for the project behind WORKOS_CLIENT_ID. Auth happens inline in the chat with no hosted pages, so there is no redirect URI to configure — but createMagicAuth errors until Magic Auth itself is switched on.

Set per GitHub Environment (staging / production). Reuse the existing RAILWAY_TOKEN, WORKOS_* and provider-key secrets; the rest are new.

NameUsed byNotes
HIRE_POSTGRES_URLhireRequired. Read at container start (migrations) and at runtime.
HIRE_REDIS_URLhireOptional. Empty values are skipped rather than sent.
LITELLM_DATABASE_URLlitellmRequired.
LITELLM_REDIS_URLlitellmRequired for the response cache.
LITELLM_MASTER_KEYbothThe same value must reach both services — hire authenticates to the proxy with it.
LITELLM_SALT_KEYlitellmEncrypts stored provider keys. Changing it makes every stored key undecryptable, so set it once per environment and leave it.
SIGNALHIRE_API_KEYhireOptional; candidate search degrades without it.
XAI_API_KEYlitellmNew — backs the grok-* models and the default alias.
WORKOS_API_KEY, WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORDhireExisting, shared with ops/pool. The cookie password must be ≥32 characters — lib/auth/workos.ts throws below that. hire scopes its cookie by name (hire_auth) so it cannot collide.
OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENAI_API_KEYlitellmExisting. Only the providers you actually route to are needed.
NameNotes
LITELLM_API_BASE_URL / LITELLM_API_BASE_URL_PRODThe litellm service URL including the /v1 suffix — e.g. https://litellm-staging.up.railway.app/v1. lib/ai/providers.ts uses it verbatim as the OpenAI base URL, and lib/litellm.ts takes its origin for the management API.
RAILWAY_PORT_HIRE / RAILWAY_PORT_HIRE_PRODOptional, defaults to 3000.
HIRE_SITE_URL / HIRE_SITE_URL_PRODThe public origin hire is served from, no trailing slash — e.g. https://hire-staging.up.railway.app. Becomes NEXT_PUBLIC_SITE_URL, which metadataBase resolves canonical and OpenGraph URLs against, and which robots.ts/sitemap.ts emit. Wrong here means wrong canonicals, so set it per environment.

Credentials that exist only in production should be generated fresh and stored in 1Password as the system of record, then copied into the production GitHub Environment. Staging can reuse the values already in local .env / .env.local.

hire’s start script is bypassed. package.json hardcodes --port 3002, which would ignore the port Railway injects, so hire-deploy.yml sets RAILPACK_START_CMD to run next start -H 0.0.0.0 -p ${PORT} directly. This is the same override ops and pool use.

Migrations run at container start, not during the build. Railway’s private network (*.railway.internal) only resolves at runtime, so a build-time migration cannot reach the database — it fails with getaddrinfo ENOTFOUND postgres.railway.internal. RAILPACK_START_CMD therefore runs tsx lib/db/migrate.ts immediately before next start. Drizzle’s migrator records what it has applied, so re-running on every restart is a no-op once the schema is current. Check the deploy log (not the build log) for Migrations completed after shipping a schema change.

Because of this, apps/hire’s build script is plain next build. Run pnpm --filter hire db:migrate by hand against a local database.

LiteLLM’s config is baked into the image. docker-compose.yml bind-mounts litellm.config.yaml; Railway has no bind mounts, so infra/litellm/Dockerfile copies it in instead. The build context is the repo root, and Railway is pointed at the Dockerfile via the RAILWAY_DOCKERFILE_PATH variable. A change to litellm.config.yaml therefore requires a litellm redeploy, not just a restart.

The litellm image tag is latest, matching docker-compose.yml so staging behaves like local dev. Pin it to a released tag before relying on it in production.

Production is gated on the pre-release backup. Both new jobs needs: the same backup job as the rest of the release, so a failed backup blocks them too.