FastAPI service for UK and US tax-benefit microsimulations. Uses Supabase for storage and Modal.com for serverless compute with sub-1s cold starts.
┌─────────────────────────────────────────────────────────────┐
│ Level 2: Reports (future) │
│ AI-generated documents, orchestrating multiple jobs │
├─────────────────────────────────────────────────────────────┤
│ Level 1: Analyses │
│ Operations on simulations (comparisons, aggregations) │
│ /analysis/economic-impact → economy_comparison_* │
├─────────────────────────────────────────────────────────────┤
│ Level 0: Simulations │
│ Single world-state calculations │
│ /household/calculate → simulate_household_* │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────┐
│ Modal.com │
│ simulate_household_uk │
│ simulate_household_us │
│ economy_comparison_uk │──▶ Supabase
│ economy_comparison_us │
└──────────────────────────┘
All compute runs on Modal.com serverless functions. The API triggers these functions and clients poll for results. See docs/DESIGN.md for the full API hierarchy design.
- Supabase CLI
- Docker and Docker Compose
- Python 3.13+ with uv
- Modal.com account
make install # install dependencies
cp .env.example .env # create env file
supabase start # start local supabase (copy anon/service keys to .env)
make init # create tables, storage bucket, RLS policies
make seed # seed UK/US models with variables, parameters, datasets
docker compose up # start API at http://localhost:8000To run simulations, deploy Modal functions:
modal token set --token-id <id> --token-secret <secret>
make modal-deployAll simulation and analysis endpoints are async: submit a request, get a job ID, poll until complete.
Calculate taxes and benefits for a single household.
Submit (UK):
curl -X POST http://localhost:8000/household/calculate \
-H "Content-Type: application/json" \
-d '{
"tax_benefit_model_name": "policyengine_uk",
"people": [{"age": 30, "employment_income": 50000}],
"year": 2026
}'Submit (US):
curl -X POST http://localhost:8000/household/calculate \
-H "Content-Type: application/json" \
-d '{
"tax_benefit_model_name": "policyengine_us",
"people": [{"age": 40, "employment_income": 70000}],
"tax_unit": {"state_code": "CA"},
"year": 2024
}'Poll:
curl http://localhost:8000/household/calculate/{job_id}Response (when complete):
{
"job_id": "...",
"status": "completed",
"result": {
"person": [{"income_tax": 7500, "national_insurance": 4500, ...}],
"household": {"household_net_income": 38000, ...}
}
}Compare baseline vs reform across a population dataset. Returns decile impacts, budget impacts, and winners/losers.
Submit:
curl -X POST http://localhost:8000/analysis/economic-impact \
-H "Content-Type: application/json" \
-d '{
"tax_benefit_model_name": "policyengine_uk",
"dataset_id": "...",
"policy_id": "..."
}'Poll:
curl http://localhost:8000/analysis/economic-impact/{job_id}Response (when complete):
{
"report_id": "...",
"status": "completed",
"decile_impacts": [...],
"program_statistics": [...]
}Create policy reforms by specifying parameter changes.
# Search for parameters
curl "http://localhost:8000/parameters?search=basic_rate"
# Create policy
curl -X POST http://localhost:8000/policies \
-H "Content-Type: application/json" \
-d '{
"name": "Lower basic rate to 16p",
"parameter_values": [{
"parameter_id": "...",
"value_json": 0.16,
"start_date": "2026-01-01T00:00:00Z"
}]
}'GET /datasets List population datasets
GET /parameters?search=... Search parameters
POST /dynamics Create behavioural response
GET /variables List variables
GET /health Health check
Copy .env.example to .env and configure. All variables are documented in .env.example.
| Variable | Description |
|---|---|
SUPABASE_URL |
Supabase API URL (https://...) |
SUPABASE_KEY |
Supabase anon/public key |
SUPABASE_SERVICE_KEY |
Supabase service role key |
SUPABASE_DB_URL |
PostgreSQL connection string |
STORAGE_BUCKET |
Supabase storage bucket name |
LOGFIRE_TOKEN |
Logfire observability token |
ANTHROPIC_API_KEY |
Anthropic API key for agent |
AGENT_USE_MODAL |
Use Modal for agent (true/false) |
For production Modal deployment, secrets are managed via Modal CLI (not .env):
modal secret create policyengine-db DATABASE_URL='...' SUPABASE_URL='...' ...
modal secret create anthropic-api-key ANTHROPIC_API_KEY='...'make format # ruff formatting
make lint # ruff linting with auto-fix
make test # unit tests
make integration-test # full integration testsmake modal-deploy # deploy serverless functionsAutomated via GitHub Actions on merge to main. The Docker build compiles the Next.js docs site and bundles it into the API image, served at /docs.
Required GitHub secrets:
SUPABASE_URL,SUPABASE_KEY,SUPABASE_DB_URLMODAL_TOKEN_ID,MODAL_TOKEN_SECRETLOGFIRE_TOKEN- GCP workload identity federation
policyengine-api-v2/
├── src/policyengine_api/
│ ├── api/ # FastAPI routers
│ ├── models/ # SQLModel database models
│ ├── services/ # Database, storage
│ ├── modal_app.py # Modal serverless functions
│ └── main.py # FastAPI app
├── supabase/migrations/ # RLS policies
├── terraform/ # Cloud Run infrastructure
├── scripts/ # Database init and seeding
└── docs/ # Next.js docs site + DESIGN.md
AGPL-3.0