Skip to content

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 Flask app object from app.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:

  1. Direct Postgres over DATABASE_URL (IPv6) — all live app reads/writes.
  2. 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

  1. Render is connected to the GitHub repository.
  2. Every push to main triggers an automatic build + deploy of the iwmi-leaf service.
  3. Render runs buildCommand (from root dir leaf_flask), then restarts under the new startCommand.

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).

cd leaf_flask
python run_migration.py migrations/001_smart_refresh.sql

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_run interval 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:

GET /health

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:

mkdocs serve                    # http://localhost:8000

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.