Deployment¶
LEAF DSS runs on Render as a single Python Web Service. The
production site is served at https://leaf-asrlm.in. Deployment
is driven by the render.yaml Blueprint committed at leaf_flask/render.yaml, and every
push to the GitHub main branch auto-deploys to production.
The backend data store is Supabase Postgres (reached over the direct DATABASE_URL
connection). Base geodata and the village master (data/villages.csv) are committed to the
repo. There is no ORM — the app uses raw SQL through a psycopg2 connection pool.
No application-level authentication
The app has no login / user auth. Admin-only actions are gated only by a ?admin=1
query parameter (or X-Admin: 1 header). Do not expose destructive endpoints to the
public internet without adding real auth or network-level protection.
Render Blueprint (render.yaml)¶
The service is defined declaratively. The file lives at leaf_flask/render.yaml, so the
Render service root directory is leaf_flask (all commands run from there).
services:
- type: web
name: iwmi-leaf
runtime: python
plan: standard
buildCommand: pip install -r requirements.txt
startCommand: gunicorn app:app --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --preload
envVars:
- key: PYTHON_VERSION
value: 3.11.0
| Setting | Value | Notes |
|---|---|---|
| Service name | iwmi-leaf |
|
| Runtime | python |
Native Python runtime (not Docker). A Dockerfile exists as an alternative but Render uses the Python path. |
| Plan | standard |
$25/mo, 1 CPU / 2 GB RAM, no spin-down. Pinned in the Blueprint so a sync can't silently drop the service back to Free (which shows a cold-start "waking up" page). |
| Python version | 3.11.0 |
Set via the only Blueprint env var, PYTHON_VERSION. |
| Build command | pip install -r requirements.txt |
Installs Python deps only. |
| Start command | gunicorn app:app --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --preload |
See below. |
Start command explained¶
app:app— gunicorn imports the Flaskappobject fromapp.py.--workers 2— two worker processes fit the 2 GB instance.--preload— the app loads shapefiles and the RAG vectorstore once, pre-fork, so the two workers share a copy-on-write memory image instead of each loading its own copy.--timeout 120— 120 s covers the heavier geopandas / feasibility requests.
Documentation site is pre-built, not built at deploy time
The build command does not run mkdocs build. The MkDocs Material site is built
locally and its output is committed to leaf_flask/site/ (per site_dir:
leaf_flask/site in mkdocs.yml). Flask serves that folder at /documentation/. To
update the written docs, run mkdocs build locally and commit the regenerated
leaf_flask/site/ along with your docs/ changes — see
Rebuilding documentation.
Environment variables & secrets¶
Only PYTHON_VERSION lives in render.yaml. All secrets are set in the Render dashboard
under the service's Environment tab (never committed). Locally they live in
leaf_flask/.env (gitignored; only .env.example is committed).
| Variable | Required | Used by | Description |
|---|---|---|---|
DATABASE_URL |
Yes | App (all cluster data) | Supabase direct Postgres connection string (IPv6). Drives the psycopg2 ThreadedConnectionPool in db.py. If unset, the app runs but has no database and the scheduler is a no-op. |
SUPABASE_URL |
Yes (for backups) | Nightly DB backup | Supabase project REST/Storage base URL (IPv4 HTTPS). |
SUPABASE_SECRET_KEY |
Yes (for backups) | Nightly DB backup | Supabase service-role key. Used only by db_backup.py (PostgREST / Storage), deliberately independent of the direct Postgres path. |
SUPABASE_PUBLISHABLE_KEY |
Recommended | Supabase client access | Supabase anon / publishable key. |
OPENAI_API_KEY |
Yes (for AI) | RAG recommendations | Required by rag_utils.py — AI recommendation calls fail without it. Model: gpt-4o-mini. |
SECRET_KEY |
Optional | Flask sessions | Flask session-signing secret. Falls back to a hard-coded default if unset — set a random value in production. |
Two separate Supabase access paths
The app reaches Supabase two ways, and both must be configured:
- Direct Postgres over
DATABASE_URL(IPv6) — all live app reads/writes. - REST / Storage over
SUPABASE_URL+SUPABASE_SECRET_KEY(IPv4) — the nightly backup only.
Rotate secrets on handover
The local .env has held live credentials during development. Rotate
DATABASE_URL, the Supabase keys, and OPENAI_API_KEY when handing the project over.
Auto-deploy from GitHub¶
- Render is connected to the GitHub repository.
- Every push to
maintriggers an automatic build + deploy of theiwmi-leafservice. - Render runs
buildCommand(from root dirleaf_flask), then restarts under the newstartCommand.
Guard cadre edits before pushing
A deploy does not touch data, but any commit that changes the clustering algorithm
(ALGO_VERSION) or the whole-state refresh can trigger regeneration that risks
overwriting hand-edited cadre clusters. Verify changes cannot wipe locked / finalized /
dashboard clusters before pushing to main. See the clustering docs for the protection
invariants.
Database migrations (manual)¶
Schema changes are not applied automatically on deploy. They are run manually with
run_migration.py, which reads DATABASE_URL from leaf_flask/.env and applies a single
SQL file. Migrations are written to be idempotent (safe to re-run).
Applied migrations, in order:
| File | Purpose |
|---|---|
001_smart_refresh.sql |
Adds clusters.locked; creates cluster_generation (fingerprint) table. |
002_provisional.sql |
Adds clusters.provisional. |
003_district_coordinator.sql |
Adds clusters.district_coordinator. |
004_cluster_name.sql |
Adds editable clusters.cluster_name. |
005_split_lakhipur.sql |
Splits LAKHIPUR into LAKHIPUR (CACHAR) / LAKHIPUR (GOALPARA) (preserves locked cadre data). |
006_split_binnakandi.sql |
Splits BINNAKANDI into BINNAKANDI (HOJAI) / BINNAKANDI (CACHAR) (preserves locked cadre data). |
Where to run migrations
run_migration.py connects with the same DATABASE_URL the app uses, so you can run it
from any machine that has network access to the Supabase Postgres host and a
leaf_flask/.env containing DATABASE_URL. The script never prints the URL or secret.
In-process scheduler & backups¶
There is no separate Render worker or cron job. start_scheduler() is called at app
import and runs a background daemon thread inside the web process (scheduler.py). It is
a no-op if DATABASE_URL is unset.
- Startup: waits 120 s after boot, then checks hourly. Each job runs at most once every 24 h.
- Concurrency safety: guarded by Postgres advisory locks plus a
maintenance_runinterval table, so only one of the two gunicorn workers ever executes a given job.
| Job | What it does |
|---|---|
| Coverage sweep | Calls villages.refresh_all_coverage() — rebuilds clusters for every block × commodity, skipping any scope that is finalized, locked, or has a dashboard (cadre-edit protection). Manual trigger: POST /api/clusters/refresh-all?admin=1 (bypasses the 24 h interval). |
| Nightly DB backup | Calls db_backup.backup_to_storage() — dumps clusters, cluster_villages, and cluster_generation to the Supabase Storage bucket db-backups as <date>/<table>.csv.gz (IPv4 HTTPS via SUPABASE_URL / SUPABASE_SECRET_KEY). Retains 14 days; no-op if Supabase creds are absent. |
Custom domain¶
Production is served at https://leaf-asrlm.in. The custom domain and its TLS are
configured in the Render dashboard (Settings → Custom Domains) with matching DNS
records at the domain registrar. This is not defined in render.yaml or any code — it
is dashboard/DNS configuration only.
The default Render URL https://iwmi-leaf.onrender.com still resolves to the same service.
Available URLs (production)¶
| URL | Description |
|---|---|
https://leaf-asrlm.in/ |
Main dashboard (map UI) |
https://leaf-asrlm.in/update |
Ops console (coverage, refresh-all, config, doc upload) |
https://leaf-asrlm.in/clustering |
Cluster planner UI |
https://leaf-asrlm.in/docs |
Swagger / Flasgger API reference |
https://leaf-asrlm.in/documentation/ |
MkDocs written documentation |
https://leaf-asrlm.in/api |
API endpoint listing |
https://leaf-asrlm.in/health |
Health check (JSON) |
Health check¶
Point Render's health check at:
Local development¶
Quick start¶
cd leaf_flask
pip install -r requirements.txt
cp .env.example .env # then fill in DATABASE_URL + secrets
python app.py
Runs on http://localhost:5000 with debug mode and auto-reload. Without a valid
DATABASE_URL the app starts but cluster features and the scheduler are inactive.
MkDocs live preview¶
For editing the written documentation with hot reload:
Changes to docs/*.md reflect instantly. This dev server is separate from the copy Flask
serves under /documentation/.
Rebuilding documentation¶
The site Flask serves is the committed leaf_flask/site/ folder. After editing anything in
docs/:
mkdocs build # regenerates leaf_flask/site/
git add docs leaf_flask/site
git commit -m "docs: update"
git push # auto-deploys to production
mkdocs.yml setting |
Value | Purpose |
|---|---|---|
site_url |
https://iwmi-leaf.onrender.com/documentation/ |
Resolves internal links under /documentation/. |
site_dir |
leaf_flask/site |
Build output — the tracked folder Flask serves. |
theme.name |
material |
Material for MkDocs theme. |
Data files¶
The app expects these files under leaf_flask/data/ (committed to the repo).
Required (core functionality)¶
| File(s) | Description |
|---|---|
villages.csv |
Village master (~21,495 rows) — source of truth for the cluster planner. |
4DSS_VAR_2.0.shp + .dbf, .shx, .prj |
Block-level shapefile with indicator data. |
Block_assam.shp + .dbf, .shx, .prj |
District–block mapping shapefile. |
DSS_input2.csv |
Variable metadata / intervention config (local fallback for the Google Sheet overlay). |
block_values.csv |
Per-block variable values (local fallback for the map / feasibility overlay). |
AI features¶
| File(s) | Description |
|---|---|
ai-docs/*.pdf |
Policy documents indexed into the Chroma vectorstore (data/vectorstore/) for RAG. |
Troubleshooting¶
| Issue | Solution |
|---|---|
| App starts but no cluster data | DATABASE_URL missing or wrong — the app degrades gracefully instead of crashing. Check the Render env vars. |
| Nightly backup skipped | SUPABASE_URL / SUPABASE_SECRET_KEY unset — backups are a no-op without them. |
| AI recommendation fails | Set OPENAI_API_KEY; ensure ai-docs/*.pdf exist so the vectorstore can build. |
| Migration needed but not applied | Migrations are manual — run python run_migration.py migrations/<file>.sql. Deploys never apply them. |
/documentation/ returns 404 |
leaf_flask/site/ isn't present — run mkdocs build and commit the output. |
| Cold-start "waking up" page | Service dropped to Free plan — confirm plan: standard in render.yaml and re-sync the Blueprint. |
| Shapefile not found | Verify .shp, .dbf, .shx, and .prj all exist in leaf_flask/data/. |
| Slow first request | Normal — the first request loads and caches shapefiles / vectorstore (--preload front-loads most of this at boot). |
leaf-asrlm.in not resolving |
Check the Custom Domain entry in the Render dashboard and the registrar's DNS records. |