This guide walks through deploying and developing your own Oddish stack.
The recommended architecture:
- Backend API + workers on Modal (serverless)
- Postgres that you control (Neon, Supabase, RDS, etc.)
- S3-compatible bucket for task bundles and trial artifacts
- Frontend dashboard on Vercel/Docker for API key management and run inspection
- Clerk for dashboard auth and org/user management
There are two workflows covered below:
- Local Development — iterate against an ephemeral Modal backend with the frontend on HTTPS so Clerk production keys work
- Production Deployment — deploy the backend to Modal and the frontend to Vercel / a container host
Both workflows share the same prerequisites, environment configuration, and migration steps. Read Prerequisites and Configure environment once, run the migrations, then jump to whichever workflow you need.
- Python
3.13anduv - Node.js
20+andpnpm - Modal account + CLI (
modal) - A Postgres connection string
- An S3-compatible bucket + access key pair
- A Clerk application (for dashboard auth)
Install and authenticate the Modal CLI:
uv pip install modal
modal setupcd backend
cp .env.example .envMinimum required values:
ODDISH_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/db
# Clerk JWT verification + webhook ingestion
CLERK_DOMAIN=clerk.your-domain.com
CLERK_SECRET_KEY=sk_...
CLERK_WEBHOOK_SECRET=whsec_...
# S3-compatible storage (required)
ODDISH_S3_BUCKET=...
ODDISH_S3_REGION=...
ODDISH_S3_ACCESS_KEY=...
ODDISH_S3_SECRET_KEY=...
ODDISH_S3_ENDPOINT_URL=...Provider keys (add the ones you plan to use). OpenAI-family jobs default to Azure OpenAI:
# Use an OpenAI-compatible endpoint such as *.openai.azure.com/openai/v1 or
# *.services.ai.azure.com/openai/v1. Do not use the Foundry project endpoint
# ending in /api/projects/<project>.
ODDISH_OPENAI_PROVIDER=azure
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=https://YOUR-RESOURCE.openai.azure.com/openai/v1
AZURE_OPENAI_API_VERSION=...
ODDISH_AZURE_OPENAI_DEPLOYMENTS='{"openai/gpt-5.2":"azure-gpt-5-2","gpt-5.2":"azure-gpt-5-2"}'
GEMINI_API_KEY=...
# To use public OpenAI instead, explicitly set:
# ODDISH_OPENAI_PROVIDER=openai
# OPENAI_API_KEY=sk-...
# oddish runs Claude exclusively through AWS Bedrock. The Modal image sets
# CLAUDE_CODE_USE_BEDROCK=1; provide the bearer token here.
AWS_BEARER_TOKEN_BEDROCK=...See backend/.env.example for the full list of optional knobs (CORS, GitHub
integration, Modal scaling, etc.).
cd frontend
cp env.example .env.localMinimum required values:
# Clerk (test keys for localhost; prod keys for the local HTTPS flow)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
CLERK_JWT_TEMPLATE=oddish
# Backend URL (see the workflow sections below)
NEXT_PUBLIC_API_URL=http://localhost:8000For the local HTTPS flow with Clerk production keys, also set:
NEXT_PUBLIC_APP_URL=https://local.oddish.appTwo migration stacks must be applied against the same database — core (from
oddish/) and cloud (from backend/):
# Core tables
cd oddish
uv run alembic upgrade head
# Cloud auth / extensions
cd ../backend
uv run alembic upgrade headRe-run these whenever you pull changes that touch either alembic/ directory.
In Clerk, create a JWT template named oddish (matches
CLERK_JWT_TEMPLATE in the frontend env) with claims:
{
"email": "{{user.primary_email_address}}",
"org_id": "{{org.id}}",
"org_role": "{{org.role}}"
}Point a Clerk webhook at your deployed backend:
https://<your-modal-workspace>--api.modal.run/webhooks/clerk
Copy the signing secret into backend/.env as CLERK_WEBHOOK_SECRET.
This workflow runs the backend on an ephemeral Modal deployment and the frontend locally on HTTPS so you can use Clerk production keys during development.
cd backend
uv sync
uv run modal serve deploy.pymodal serve hot-reloads on code changes and prints a URL like
https://<workspace>--api-dev.modal.run. Keep this terminal open.
The frontend needs to run on an oddish.app subdomain with trusted TLS so
Clerk's production keys accept the origin. frontend/run-prod-clerk-local.sh
handles cert generation (via mkcert) and launches Next.js on port 443.
One-time setup:
# 1. Add the subdomain to /etc/hosts
echo "127.0.0.1 local.oddish.app" | sudo tee -a /etc/hosts
# 2. Install mkcert and the local CA
# macOS: brew install mkcert
# Linux: use your distro package manager, or see https://github.com/FiloSottile/mkcert
mkcert -installPoint the frontend at the Modal dev URL from step 1 and use Clerk production
keys (so JWTs are accepted by Clerk across the oddish.app origin):
NEXT_PUBLIC_API_URL=https://<workspace>--api-dev.modal.run
NEXT_PUBLIC_APP_URL=https://local.oddish.app
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
CLERK_JWT_TEMPLATE=oddishThe dev server listens on port 443 and caches its build in .next/. Because
port 443 requires elevated privileges, the script re-execs itself under sudo
— which means any subsequent pnpm dev on the same .next/ directory will
hit permission errors. Blow away .next/ before each HTTPS run:
cd frontend
sudo rm -rf .next && ./run-prod-clerk-local.shThen open https://local.oddish.app.
If you instead want a plain
http://localhost:3000dev loop with Clerk test keys, skip the HTTPS flow and runpnpm dev. That's usually simpler when you don't need production Clerk behavior.
export ODDISH_API_URL="https://<workspace>--api-dev.modal.run"
export ODDISH_API_KEY="ok_..." # created from the dashboard Settings page
oddish statusProvision your Modal secret (named oddish-prod by default, override with
RUNTIME_SECRET_NAME in modal_app.py) with the same values you put in
backend/.env. Then:
cd backend
uv run modal deploy deploy.pyThat publishes the stable API + workers:
https://<workspace>--api.modal.run
Wire up the Clerk webhook (see Configure Clerk) against this URL and re-run migrations if needed.
Deploy to Vercel. Set these env vars in the hosting platform:
NEXT_PUBLIC_API_URL=https://<workspace>--api.modal.run
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
CLERK_JWT_TEMPLATE=oddishexport ODDISH_API_URL="https://<workspace>--api.modal.run"
export ODDISH_API_KEY="ok_..." # created from the dashboard Settings page
oddish run -d terminalbench@2.0 -a codex -m openai/gpt-5.4-mini --n-trials 3Per-model concurrency can be tuned via backend env vars:
ODDISH_DEFAULT_MODEL_CONCURRENCY=64
ODDISH_MODEL_CONCURRENCY_OVERRIDES='{"openai/gpt-5.2": 64, "anthropic/claude-sonnet-4-5": 32}'