Skip to content

Self-hosting

Overturn is one API service (NestJS, Node 22) and one web app (Next.js) on Postgres 16 with pgvector. Background jobs (replay, implicit approval, cache purge) run on pg-boss inside the API process. No Redis, no queues to operate.

Terminal window
cp .env.example .env
docker compose up -d
open http://localhost:3000

The API runs migrations at boot. Sign in with your email: without RESEND_API_KEY the magic link is printed in the API log (docker compose logs api) and shown on the login page.

VariableServicePurpose
DATABASE_URLapiPostgres 16 with CREATE EXTENSION vector allowed
PORTapidefault 4000
WEB_URLapipublic URL of the web app, used in magic links and cookie settings
AUTH_SECRETapiany long random string
RESEND_API_KEY, EMAIL_FROMapimagic-link email; optional in dev
OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OPENROUTER_API_KEYapireplay targets and the judge/embedding models
JOBS_DISABLEDapi1 to run without pg-boss (replays then run inline in the request process)
LOG_LEVELapifatal … trace
OVERTURN_API_URLwebthe URL the browser uses to reach the API (read at runtime)

Cookies: when WEB_URL is https://…, the session cookie is Secure; SameSite=None so the web app and API can live on different hosts. Put both behind one domain (app.example.com and api.example.com) or the same reverse proxy.

See railway/README.md: a Postgres plugin plus two services built from apps/api/Dockerfile and apps/web/Dockerfile. A deploy workflow (.github/workflows/deploy-railway.yml) ships main when RAILWAY_TOKEN and the RAILWAY_DEPLOY=true variable are set.

Terminal window
pnpm install && pnpm build
pnpm db:migrate
pnpm --filter @overturn-ai/api start # node apps/api/dist/main.js
OVERTURN_API_URL=http://localhost:4000 pnpm --filter @overturn-ai/web start
  • Migrations are plain SQL in apps/api/drizzle/, applied by pnpm db:migrate (or at container boot). Never auto-applied by the running API.
  • Retention: a daily job (03:23 UTC) deletes runs older than retentionDays that back no case, with their steps and corrections. Runs behind a case are kept: they are the evidence.
  • Review webhook: reviewWebhookUrl (Slack incoming-webhook format) gets every completed run once (runs.notified_at); a failed POST is logged, not retried.
  • Cache: model responses for replay live in response_cache (24h TTL, purged hourly).
  • Backups: it is one Postgres database; pg_dump is the whole story.
  • Scaling: the API is stateless apart from pg-boss, which coordinates through Postgres; run several replicas behind a load balancer if needed.