Thin coordination layer for the fAIr AI-Assisted Mapping platform. Owns the public REST API, the user database, and the orchestration of dataset builds, training runs and predictions. ML pipelines and STAC catalog operations live in fair-py-ops and run on a ZenML stack.
Runtime dependencies live in per-model images that ZenML and Knative pull. The
backend image also bundles the matching fAIr-models/models/ pipeline modules;
registration rejects a model whose entrypoint is not in that bundle.
The compose file at the repository root runs this backend and everything it depends on. See Run locally.
Useful when iterating on backend code. Start the dependencies in docker, then run Django outside it:
cd ..
docker compose up -d postgres minio stac mlflow zenml
cd backend
just setup
cp env_example .env
just migrate
just run
just worker # runs async jobs (django_tasks)The two sample files differ only in host names: the root env_example uses
compose service names (postgres, minio, stac, zenml), while
backend/env_example uses localhost with the ports those services publish.
OpenAPI schema at /api/schema/, Swagger UI at /api/docs/, ReDoc at /api/redoc/.
.env is read by pydantic-settings. Empty values are treated as unset and fall back to defaults. Required vars raise at process boot if missing. SecretStr values are masked in tracebacks. env_example mirrors the tables below.
| Name | Required | Default | Description |
|---|---|---|---|
DEBUG |
no | false |
Django debug mode. In prod (false), SECRET_KEY must be at least 32 chars and not contain dev or unsafe. |
SECRET_KEY |
yes | (none) | Django secret. Strict length/strength check when DEBUG=false. |
DATABASE_URL |
yes | (none) | Postgres URL. Scheme must be postgres, postgresql, or postgis. |
DATABASE_SSL_MODE |
no | null |
psycopg sslmode. Set to require for hosted Postgres. |
ALLOWED_HOSTS |
no | [] |
Comma-separated list. |
CSRF_TRUSTED_ORIGINS |
no | [] |
Comma-separated list. |
CORS_ALLOWED_ORIGINS |
no | [] |
Comma-separated list. |
SECURE_SSL_REDIRECT |
no | true |
Force HTTPS redirect at the Django layer. |
FRONTEND_URL |
yes | (none) | Public URL of the SPA. Used in emails and CORS. |
API_BASE_URL |
yes | (none) | Public URL of this backend (e.g. http://localhost:8000/api/v1). |
HOSTNAME |
no | 127.0.0.1 |
Used by the OpenAPI server URL. |
AUTH_PROVIDER selects the auth backend. Both share one contract: Authorization: Bearer <token>. hanko (production) validates a per-user JWT issued by Hanko (sent via Bearer header or hanko cookie). dev (local only) compares the Bearer token against the static FAIR_DEV_TOKEN; anyone with the token gets full dev-user access. Same header in dev and prod, only the issuer differs.
GET on datasets, local-models, and predictions is open to anonymous callers for rows with visibility="public". Owner-scoped lifecycle data (AOIs, trainings, feedback, notifications) and every write require Bearer auth.
| Name | Required | Default | Description |
|---|---|---|---|
AUTH_PROVIDER |
no | hanko |
One of hanko, dev. |
FAIR_DEV_TOKEN |
when AUTH_PROVIDER=dev |
null |
Static dev token. Generate with openssl rand -hex 32. Never commit. |
HANKO_API_URL |
when AUTH_PROVIDER=hanko |
null |
Hanko backend URL. |
COOKIE_SECRET |
when AUTH_PROVIDER=hanko |
null |
Used to verify Hanko-signed cookies. |
COOKIE_DOMAIN |
no | null |
Cookie scope domain. |
COOKIE_SECURE |
no | null |
Force Secure cookie flag. |
JWT_AUDIENCE |
no | null |
Expected aud claim. |
LOGIN_URL |
no | https://login.hotosm.org |
HOT login portal URL. |
LOGIN_INTERNAL_API_KEY |
no | null |
Server-to-server key against the login portal. |
LOGIN_BACKEND_URL |
no | null |
Login portal backend URL. |
OSM_LOGIN_REDIRECT_URI |
no | null |
Required only when Hanko's "connect existing OSM account" flow is enabled. |
FAIR_* values are read by this backend. The ZENML_STORE_* values are read by
the zenml library itself when it opens a connection, so both sets point at the
same server. Authenticate with either an API key or a username and password.
| Name | Required | Default | Description |
|---|---|---|---|
FAIR_ZENML_STORE_URL |
yes (at runtime) | null |
URL of the deployed ZenML server. Optional at boot, raises loud at first call site. |
FAIR_STAC_API_URL |
yes (at runtime) | null |
URL of the STAC API root (eoapi-stac-fastapi). Trailing slashes are stripped. |
FAIR_STAC_API_KEY |
prod | null |
Bearer token for the STAC Transactions extension. |
FAIR_STAC_DSN |
prod | null |
pgstac Postgres DSN. When set, STAC writes go to pgstac directly, so the API can be read-only. |
ZENML_STORE_URL |
yes (at runtime) | null |
Same server as FAIR_ZENML_STORE_URL. |
ZENML_STORE_API_KEY |
one of the two | null |
Service-account key. Mint with zenml service-account create fair-backend. |
ZENML_STORE_USERNAME |
one of the two | null |
Username, paired with ZENML_STORE_PASSWORD. The compose stack's default user is default with an empty password. |
ZENML_STORE_PASSWORD |
with username | null |
Password for the above. |
| Name | Required | Default | Description |
|---|---|---|---|
BUCKET_NAME |
yes (at runtime) | null |
S3 / MinIO bucket. |
PARENT_BUCKET_FOLDER |
no | dev |
Per-environment prefix inside the bucket. |
AWS_REGION |
no | us-east-1 |
S3 region. |
AWS_ACCESS_KEY_ID |
yes (at runtime) | null |
|
AWS_SECRET_ACCESS_KEY |
yes (at runtime) | null |
|
AWS_ENDPOINT_URL |
no | null |
Set for non-AWS S3 (MinIO, Cloudflare R2, etc.). |
PRESIGNED_URL_EXPIRY |
no | 900 |
Presigned-URL TTL in seconds. |
DRF throttle scopes use <count>/<period>, e.g. 1000/h. Use the Django-native pool OR PgBouncer, not both.
| Name | Required | Default | Description |
|---|---|---|---|
USER_RATE_LIMIT |
no | 1000/h |
Authenticated requests. |
ANON_RATE_LIMIT |
no | 100/h |
Anonymous requests. |
TRAINING_RATE_LIMIT |
no | 10/h |
Training submission throttle. |
PREDICTION_RATE_LIMIT |
no | 50/h |
Prediction submission throttle. |
DB_POOL_MIN_SIZE |
no | 4 |
Django 5.1+ native PostgreSQL connection pool minimum. |
DB_POOL_MAX_SIZE |
no | 20 |
Pool maximum. |
DB_POOL_TIMEOUT |
no | 30 |
Seconds to wait for a free connection. |
| Name | Required | Default | Description |
|---|---|---|---|
RAW_DATA_API_URL |
no | https://api-prod.raw-data.hotosm.org/v1 |
HOT Raw Data API root. |
Off by default. When ENABLE_MAPSWIPE=false, POST /api/v1/predictions/<id>/mapswipe/ returns 503. The /api/v1/health/ endpoint reports mapswipe.reachable when enabled.
| Name | Required | Default | Description |
|---|---|---|---|
ENABLE_MAPSWIPE |
no | false |
Master toggle. |
MAPSWIPE_BACKEND_URL |
no | null |
Mapswipe backend URL. |
MAPSWIPE_MANAGER_URL |
no | null |
Mapswipe manager URL. |
MAPSWIPE_WEB_URL |
no | null |
Mapswipe web app URL. |
MAPSWIPE_CSRFTOKEN_KEY |
when ENABLE_MAPSWIPE=true |
null |
CSRF cookie name on the Mapswipe backend. |
MAPSWIPE_FB_AUTH_URL |
when ENABLE_MAPSWIPE=true |
null |
Firebase auth endpoint. |
MAPSWIPE_FB_USERNAME |
when ENABLE_MAPSWIPE=true |
null |
Firebase service-account username. |
MAPSWIPE_FB_PASSWORD |
when ENABLE_MAPSWIPE=true |
null |
Firebase service-account password. |
MAPSWIPE_TUTORIAL_ID |
no | 37 |
Mapswipe tutorial ID injected into pushed projects. |
MAPSWIPE_ORGANIZATION_ID |
no | 4 |
Mapswipe organization ID. |
MAPSWIPE_VERIFICATION_NUMBER |
no | 3 |
Number of crowd verifications required per tile. |
MAPSWIPE_POLL_INTERVAL |
no | 10 |
Seconds between Mapswipe push-status polls. |
MAPSWIPE_POLL_TIMEOUT |
no | 600 |
Maximum total seconds to wait for a Mapswipe push. |
Only checked when DEBUG=false.
| Name | Required | Default | Description |
|---|---|---|---|
EMAIL_HOST |
no | smtp.gmail.com |
SMTP host. |
EMAIL_PORT |
no | 587 |
SMTP port. |
EMAIL_USE_TLS |
no | true |
STARTTLS. |
EMAIL_USE_SSL |
no | false |
Implicit TLS (mutually exclusive with EMAIL_USE_TLS). |
EMAIL_HOST_USER |
no | "" |
SMTP username. |
EMAIL_HOST_PASSWORD |
no | "" |
SMTP password. |
DEFAULT_FROM_EMAIL |
no | no-reply@fair.hotosm.org |
From: header for outbound mail. |
| Name | Required | Default | Description |
|---|---|---|---|
LOG_PATH |
no | ./logs |
Directory for rotating log files. |
DEFAULT_PAGINATION_SIZE |
no | 50 |
DRF page size. |
SESSION_COOKIE_AGE |
no | 3600 |
Session cookie TTL in seconds. |
CACHE_TIMEOUT_MINUTES |
no | 5 |
Default LocMem cache TTL for non-STAC entries. |
LOG_LINE_STREAM_TRUNCATE_VALUE |
no | 10 |
Per-step log-line truncation factor for streamed pipeline logs. |
All have safe defaults; only set to override.
| Name | Required | Default | Description |
|---|---|---|---|
HEALTH_PROBE_TIMEOUT |
no | 2.0 |
Seconds each HTTP probe waits before marking a dependency unreachable in GET /api/v1/health/. |
PREDICTION_SYNC_INTERVAL |
no | 15 |
Seconds between prediction-status poll re-enqueues. |
TRAINING_SYNC_INTERVAL |
no | 30 |
Seconds between training-status poll re-enqueues. |
STAC_CACHE_TTL |
no | 300 |
Seconds the LocMem cache holds a STAC item before re-fetching. Raise in prod for lower STAC load; lower in dev to see property changes faster. |
STAC_BULK_FETCH_WORKERS |
no | 16 |
Thread pool size for parallel STAC item fetches when ?expand=stac is used on list endpoints. |
PMTILES_MIN_ZOOM |
no | 10 |
Min zoom passed to tippecanoe for PMTiles generation. |
PMTILES_MAX_ZOOM |
no | 20 |
Max zoom passed to tippecanoe for PMTiles generation. |
Background work uses Django's built-in django.tasks framework with the django-tasks-db backend (Postgres-backed, no Redis required).
just worker # runs python manage.py db_workerjust test # pytest
just lint # pre-commit run --all-files (ruff + format + ty + uv-lock-check + commitizen)Tests under backend/tests/ import across apps and mock shared.integrations.zenml; nothing hits a live ZenML server. For end-to-end checks against a running stack, run test.py, which drives the public API end to end.