Hosting & Handover Guide¶
This guide is for anyone who needs to host LEAF DSS independently - whether you're a government IT team, an NGO partner, a research institution, or a developer taking over the project. It covers what you need, your hosting options, estimated costs, and step-by-step instructions.
The live production instance is https://leaf-asrlm.in (hosted on Render, auto-deploying from the main branch).
What You're Hosting¶
LEAF DSS is a single Python web application (Flask + gunicorn) that serves both the interactive map UI and the REST API. There is no separate frontend deployment - everything runs from one process.
This app has a managed database dependency - it is NOT CSV-only
Earlier versions of this guide claimed there was "no database." That is wrong. LEAF DSS stores all cluster/cadre data in a managed PostgreSQL database (Supabase) reached over a DATABASE_URL connection string. The CSV/shapefile files in data/ are read-only base geodata (village master, block boundaries); the cluster planner output, cadre assignments, finalize/lock flags, and dashboards all live in Postgres. You cannot host LEAF DSS without provisioning a Postgres database and setting DATABASE_URL.
The two moving parts¶
| Part | What it is | Where it lives |
|---|---|---|
| Web app | Flask + gunicorn (the iwmi-leaf service) |
Render (or your host) |
| Database | Managed Postgres holding clusters, cluster_villages, cluster_generation, infrastructure, maintenance_run | Supabase project |
The app talks to Supabase over two independent paths, and both must be configured:
- Postgres (app runtime): the direct connection string in
DATABASE_URL(Supabase's IPv6 direct connection). All reads/writes go through this. - REST + Storage (backups only):
SUPABASE_URL+SUPABASE_SECRET_KEY(IPv4 HTTPS). Used only by the nightly backup job to write snapshots to Supabase Storage - deliberately independent of the IPv6 Postgres path so backups still run if IPv6 is flaky.
System Requirements¶
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 2 vCPUs |
| RAM | 512 MB | 1-2 GB |
| Disk | 500 MB | 1 GB |
| Python | 3.11 | 3.11 |
| OS | Any Linux | Ubuntu 22.04+ |
| Database | Postgres 14+ (managed) | Supabase (current setup) |
RAM is the main app-process constraint - the app loads shapefiles into memory (~10-15 MB cached) and, with the AI/RAG feature enabled, ChromaDB + embeddings push usage toward ~500 MB. The current production instance runs the Render Standard plan (2 GB, 1 CPU, no spin-down) with --workers 2 --preload. Python is pinned to 3.11.
What's Included¶
| Component | Description | Required? |
|---|---|---|
| Flask web app | Map UI + REST API + Swagger docs (/docs) |
Yes |
| Postgres database | Clusters, cadre assignments, generation fingerprints, infrastructure | Yes |
| Shapefiles & CSV | Block/district/protected-area geodata + villages.csv village master |
Yes |
| DB migrations | migrations/*.sql, applied manually via run_migration.py |
Yes (on first deploy / schema change) |
| Nightly DB backup | db_backup.py → Supabase Storage bucket db-backups |
Recommended |
| Google Sheets overlay | Published-CSV sheets for intervention config + per-block map values | Yes (live data source) |
| MkDocs site | Written documentation (pre-built HTML under leaf_flask/site/) |
Optional |
| AI/RAG pipeline | Policy-document recommendations (OpenAI) | Optional |
| PDF policy docs | Source documents for AI (ai-docs/*.pdf) |
Only if AI enabled |
Environment Variables¶
Set these on whichever host you use. On Render they are set in the dashboard (not in render.yaml). Locally they go in leaf_flask/.env (which run_migration.py and the app both read). .env is gitignored - never commit live secrets; only .env.example belongs in Git.
| Variable | Required? | Purpose |
|---|---|---|
DATABASE_URL |
Yes | Supabase direct Postgres connection string (IPv6). Everything the app reads/writes. Without it the app runs but the scheduler is a no-op and cluster data cannot persist. |
SUPABASE_URL |
For backups | Supabase project URL. Used by the nightly backup (REST/Storage, IPv4). |
SUPABASE_SECRET_KEY |
For backups | Supabase service (secret) key. Used with SUPABASE_URL for backups. |
SUPABASE_PUBLISHABLE_KEY |
Optional | Supabase anon/publishable key (client-side use). |
OPENAI_API_KEY |
AI feature only | OpenAI key for the AI recommendation feature. rag_utils raises if the feature is used without it. |
JIRA_EMAIL / JIRA_API_TOKEN |
Optional | Jira integration for issue reporting. |
SECRET_KEY |
Optional | Flask session secret. Set a random value in production. |
Rotate secrets on handover
The live .env and Render dashboard hold production database credentials and API keys. When handing over, rotate DATABASE_URL, SUPABASE_SECRET_KEY, and OPENAI_API_KEY (regenerate them in Supabase / OpenAI) so old copies stop working.
Database Setup (required on any host)¶
- Provision Postgres. Create a Supabase project (or any managed Postgres 14+). Copy the direct connection string into
DATABASE_URL. - Create the schema. Apply
leaf_flask/schema.sqlto create the base tables (clusters,cluster_generation,cluster_villages,infrastructure,maintenance_run). -
Apply migrations, in order. Migrations are manual and idempotent - they are not run automatically on deploy:
cd leaf_flask python run_migration.py migrations/001_smart_refresh.sql python run_migration.py migrations/002_provisional.sql python run_migration.py migrations/003_district_coordinator.sql python run_migration.py migrations/004_cluster_name.sql python run_migration.py migrations/005_split_lakhipur.sql python run_migration.py migrations/006_split_binnakandi.sqlrun_migration.pyreadsDATABASE_URLfromleaf_flask/.env, applies the SQL, and prints a verification line. Migrations 005/006 split the collision-proneLAKHIPURandBINNAKANDIblock names into district-qualified blocks while preserving locked cadre data - run them on any fresh copy of the database.
Cadre data is precious - migrations and refreshes protect it
Cluster rows carry locked, finalized, provisional flags and a dashboard JSONB column. Any operation that regenerates clusters (the scheduler sweep, the "Refresh all" button, CSV import reconciliation) skips scopes that are finalized, locked, or have a dashboard, so hand-curated cadre assignments are never wiped. Preserve this invariant if you modify refresh logic.
Backups¶
A nightly backup job (db_backup.py, driven by the in-process scheduler) dumps the clusters, cluster_villages, and cluster_generation tables to the Supabase Storage bucket db-backups as <YYYY-MM-DD>/<table>.csv.gz, retaining 14 days. It runs over IPv4 HTTPS using SUPABASE_URL + SUPABASE_SECRET_KEY, and is a no-op if those creds are absent. To restore, download the gzipped CSVs from the bucket and COPY them back into Postgres.
Hosting Options & Costs¶
Every option below also needs a Postgres database
The costs below are for the web app only. Add a managed Postgres instance (Supabase has a usable free tier; paid Supabase/RDS/Cloud SQL start around $10-25/month). The current production setup uses Supabase's managed Postgres.
Option 1: Render (Current Setup) - Recommended¶
Render is a managed platform that auto-deploys from GitHub. This is how https://leaf-asrlm.in runs today. The service is defined by leaf_flask/render.yaml.
| Plan | Monthly Cost | RAM | CPU | Notes |
|---|---|---|---|---|
| Free | $0 | 512 MB | Shared | Sleeps after 15 min inactivity, cold starts ~30s |
| Starter | $7/month | 512 MB | Shared | Always on, no sleep |
| Standard | $25/month | 2 GB | 1 vCPU | Current production plan - no spin-down |
| Pro | $85/month | 4 GB | 2 vCPUs | For heavy AI/RAG usage |
Pros: Zero server management, auto-deploy from Git, free SSL, health checks. Cons: Free tier has cold starts; US/EU regions only.
Keep the plan pinned to Standard
render.yaml pins plan: standard on purpose - a Blueprint sync must not silently drop the service back to Free (which shows the "SERVICE WAKING UP" cold-start page and, being ephemeral, only matters more once you rely on the always-on scheduler).
Setup Steps¶
- Push code to a GitHub repository.
- Provision a Supabase project and apply the schema + migrations (see Database Setup).
- Sign up at render.com and click New → Web Service → connect your GitHub repo. Render reads
leaf_flask/render.yaml, so root/build/start are already configured:- Build Command:
pip install -r requirements.txt - Start Command:
gunicorn app:app --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --preload
- Build Command:
- Add environment variables in the Render dashboard (see Environment Variables):
DATABASE_URL,SUPABASE_URL,SUPABASE_SECRET_KEY,SUPABASE_PUBLISHABLE_KEY,OPENAI_API_KEY, optionalSECRET_KEY. - Configure the custom domain
leaf-asrlm.inunder the service's Settings → Custom Domains and point DNS accordingly. (The domain is configured in Render/DNS, not in code.) - Click Deploy. Auto-deploy from
mainis enabled - every push tomainredeploys production.
The scheduler is in-process - no separate worker needed
Background jobs (nightly cluster-coverage sweep + nightly DB backup) run on an in-process daemon thread started at import. There is no separate Render cron/worker. With 2 gunicorn workers, a Postgres advisory lock ensures each job runs once, not twice.
Option 2: AWS (EC2 + Elastic Beanstalk or ECS)¶
Best for organizations already on AWS with IT teams. Pair the app tier with Amazon RDS for PostgreSQL (or keep using Supabase).
| Service | Monthly Cost (estimate) | Notes |
|---|---|---|
| EC2 t3.micro | ~$8/month | 1 vCPU, 1 GB RAM, free tier eligible (1 year) |
| EC2 t3.small | ~$15/month | 2 vCPU, 2 GB RAM - recommended |
| RDS db.t3.micro (Postgres) | ~$13-15/month | Managed Postgres if not using Supabase |
| Elastic Beanstalk | EC2 cost + $0 | Managed deployment wrapper around EC2 |
| ECS Fargate | ~$15-30/month | Serverless containers, no EC2 management |
| Application Load Balancer | ~$16/month | Add if you need custom domain + SSL |
| Route 53 (DNS) | ~$0.50/month | Custom domain routing |
Total estimate: $15-50/month (app) + Postgres.
Quick EC2 Setup¶
# On a fresh Ubuntu 22.04 EC2 instance
sudo apt update && sudo apt install -y python3-pip python3-venv nginx
# Clone and setup
git clone <repo-url> /opt/iwmi-leaf
cd /opt/iwmi-leaf/leaf_flask
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install mkdocs-material && mkdocs build # optional: build docs site
# Configure the database connection + secrets
cp .env.example .env
# then edit .env to add DATABASE_URL, SUPABASE_URL, SUPABASE_SECRET_KEY, OPENAI_API_KEY
# Apply schema + migrations against your Postgres (first deploy only)
python run_migration.py migrations/001_smart_refresh.sql # ...through 006
# Run with Gunicorn
gunicorn app:app --bind 0.0.0.0:5000 --workers 2 --timeout 120 --preload --daemon
# (Optional) Set up Nginx reverse proxy for port 80/443
Use a process manager
In production, use systemd or supervisord to keep Gunicorn running and auto-restart on crashes. See the systemd section below. On a self-managed host the in-process scheduler runs inside gunicorn, so the process must stay up for nightly jobs to fire.
Option 3: Google Cloud Platform (GCP)¶
Pair with Cloud SQL for PostgreSQL or keep Supabase.
| Service | Monthly Cost (estimate) | Notes |
|---|---|---|
| Cloud Run | ~$5-15/month | Serverless containers, pay per request, auto-scaling |
| Compute Engine e2-small | ~$13/month | 2 vCPU, 2 GB RAM |
| App Engine (Standard) | ~$0-25/month | Managed platform, auto-scales to zero |
| Cloud SQL (Postgres, db-f1-micro) | ~$10-15/month | Managed Postgres if not using Supabase |
Cloud Run scales to zero - watch the scheduler
Cloud Run and App Engine idle instances to zero. The in-process nightly scheduler won't fire on a container that isn't running. If you use scale-to-zero, run the coverage sweep and DB backup from an external Cloud Scheduler job (hit POST /api/clusters/refresh-all?admin=1 and trigger a backup) instead of relying on the in-process thread.
Cloud Run Setup¶
# Build container (Dockerfile is at leaf_flask/Dockerfile)
docker build -t iwmi-leaf leaf_flask/
docker tag iwmi-leaf gcr.io/YOUR_PROJECT/iwmi-leaf
docker push gcr.io/YOUR_PROJECT/iwmi-leaf
# Deploy (set env vars including DATABASE_URL)
gcloud run deploy iwmi-leaf \
--image gcr.io/YOUR_PROJECT/iwmi-leaf \
--port 5000 \
--memory 1Gi \
--cpu 1 \
--set-env-vars "DATABASE_URL=...,SUPABASE_URL=...,SUPABASE_SECRET_KEY=...,OPENAI_API_KEY=..." \
--allow-unauthenticated
Option 4: Azure¶
Pair with Azure Database for PostgreSQL.
| Service | Monthly Cost (estimate) | Notes |
|---|---|---|
| App Service B1 | ~$13/month | 1 vCPU, 1.75 GB RAM |
| App Service B2 | ~$26/month | 2 vCPU, 3.5 GB RAM |
| Container Instances | ~$10-20/month | Serverless containers |
| Azure Database for PostgreSQL (Flexible, B1ms) | ~$12-15/month | Managed Postgres |
Option 5: DigitalOcean / Hetzner (Budget VPS)¶
Best value for money if you're comfortable with basic Linux administration. Use DigitalOcean/Hetzner Managed Postgres, or keep Supabase.
| Provider | Plan | Monthly Cost | Specs |
|---|---|---|---|
| DigitalOcean | Basic Droplet | $6/month | 1 vCPU, 1 GB RAM, 25 GB SSD |
| DigitalOcean | Regular Droplet | $12/month | 2 vCPU, 2 GB RAM, 50 GB SSD |
| Hetzner | CX22 | €4/month (~$4.50) | 2 vCPU, 4 GB RAM, 40 GB - best value |
| DigitalOcean App Platform | Basic | $5/month | Managed platform (like Render) |
| Managed Postgres (either) | Starter | ~$15/month | If not using Supabase |
Option 6: On-Premises / Government Data Center¶
For government agencies or organizations with internal hosting infrastructure.
| Requirement | Details |
|---|---|
| App server | Any Linux server with Python 3.11, 2 GB RAM |
| Database | A reachable PostgreSQL 14+ instance (on-prem or managed); set DATABASE_URL to it |
| Network | HTTP/HTTPS on ports 80/443; outbound to Postgres, and to Supabase Storage + OpenAI/Google Sheets if those features are used |
| SSL | Let's Encrypt (free) or organization's certificate |
| Backup | Back up the Postgres database (or use db_backup.py against a Supabase project), plus the data/ and ai-docs/ directories |
| Updates | Pull from Git, apply any new migrations, restart the service |
If you drop Supabase Storage backups
On-prem you may not have Supabase Storage. In that case set up your own Postgres backups (e.g. nightly pg_dump) - db_backup.py becomes a no-op without SUPABASE_URL/SUPABASE_SECRET_KEY.
Cost Summary¶
| Scenario | Monthly Cost | Best For |
|---|---|---|
| Free / testing | $0 | Demos, evaluation (Render free tier + Supabase free tier) |
| Small production | $20-30/month | Low traffic (Render Starter/Standard + Supabase) |
| Standard production | $30-45/month | Public-facing, reliable uptime (current: Render Standard $25 + Supabase) |
| With AI features | +$5-20/month | OpenAI API usage on top of hosting |
| On-premises | $0 (infra cost) | Government/institutional hosting |
OpenAI API Costs
The AI recommendation feature calls the OpenAI API (gpt-4o-mini) per request. Typical cost is $0.01-0.05 per recommendation. With 100 recommendations/month, expect ~$1-5/month in API fees on top of hosting. You can also swap in a self-hosted LLM to avoid this cost.
Handover Checklist¶
If you're handing this project to another team, ensure they have:
1. Source Code¶
- [ ] Access to the Git repository (GitHub) - the
mainbranch is what Render auto-deploys - [ ]
render.yamlreviewed (leaf_flask/render.yaml)
2. Database & Credentials (most important)¶
- [ ] Supabase project access (or ownership transfer)
- [ ]
DATABASE_URL- the direct Postgres connection string (rotate on handover) - [ ]
SUPABASE_URL+SUPABASE_SECRET_KEY+SUPABASE_PUBLISHABLE_KEY - [ ] Confirmation that
schema.sql+ all sixmigrations/*.sqlhave been applied - [ ] Backup location: Supabase Storage bucket
db-backups(<date>/<table>.csv.gz, 14-day retention) - and how to restore from it - [ ]
OPENAI_API_KEY(if AI feature is used) andJIRA_EMAIL/JIRA_API_TOKEN(if Jira is used)
3. Live Data Sources¶
- [ ] Edit access to the two Google Sheets the app reads live:
- the block-values sheet (per-block variable values = the map/feasibility overlay)
- the villages / user-update sheet (friendly end-user overlay)
- plus the intervention config (dss_input) sheet
- [ ] The published-CSV URLs are configured in
google_sheets.py(SHEET_URLS); local fallbacks aredata/DSS_input2.csvanddata/block_values.csv
4. Base Data Files (in Git)¶
- [ ]
leaf_flask/data/villages.csv- the cluster planner's source of truth (~21,495 village rows) - [ ]
leaf_flask/data/4DSS_VAR_2.0.*(block shapefile - .shp, .dbf, .shx, .prj, .qmd) - [ ]
leaf_flask/data/Block_assam.*(district mapping shapefile) - [ ]
leaf_flask/data/districts.geojson - [ ]
leaf_flask/data/protected_areas/Protected_Area_India_Final.* - [ ]
leaf_flask/data/DSS_input2.csvanddata/block_values.csv(Google Sheet fallbacks) - [ ]
ai-docs/*.pdf(policy documents for AI, if used) - [ ] The village-master source workbook
source_data/SHG_Assam_Consolidated_final_Jun22.xlsx- gitignored (PII), must be transferred out-of-band to anyone regeneratingvillages.csv
5. Documentation¶
- [ ] This documentation site (
docs/, served at/documentation/from pre-builtleaf_flask/site/) - [ ] Swagger API docs (auto-generated at
/docs)
6. Knowledge Transfer¶
- [ ] The
/updateops console (see below) - how to refresh clusters and edit base data - [ ] The cadre-edit protection invariant (locked/finalized/dashboard scopes are never wiped)
- [ ] How to apply a new migration (
python run_migration.py migrations/NNN.sql) - [ ] How the nightly scheduler works (in-process; coverage sweep + DB backup)
- [ ] Assam covers 35 districts / 220 blocks
Updating Data¶
Day-to-day data changes do not require code edits. Almost everything is driven from the /update ops console and two Google Sheets.
The /update ops console¶
Visit https://leaf-asrlm.in/update. It provides:
- Refresh all clusters -
POST /api/clusters/refresh-all?admin=1. Regenerates cluster coverage across all blocks × 6 commodities, skipping any scope that is finalized, locked, or has a dashboard (cadre edits are protected). - Cluster coverage / pending renames -
GET /api/clusters/coverage,GET /api/clusters/pending-renames. - Config validation & sheet status -
GET /api/config/validate,GET /api/config/sheets-status, andPOST /api/config/refreshto force-refresh the Google Sheet cache (otherwise a 5-minute TTL applies). - Download links -
/api/clusters/export.csv,/api/clusters/unassigned.csv,/api/livestock-subfilter.csv. - Links to the two Google Sheets for editing base data.
Editing map / feasibility values (Google Sheets)¶
The map overlay and feasibility variables are read live from published Google Sheets, not from a CSV on disk:
- Edit the block-values sheet (per-block variable values) or the villages/user-update sheet (friendly end-user overlay). The user-update sheet overlays the coded block-values by block name (user values win).
- Values are cached for 5 minutes; use Refresh on
/update(POST /api/config/refresh) to pull immediately. data/block_values.csvanddata/DSS_input2.csvare only fallbacks used when the sheets are unreachable.
Run GET /api/config/validate (or the Validate button) after edits - it flags missing ID columns, duplicate block IDs, stray non-numeric values, inverted range_min > range_max, and cross-sheet issues.
Updating the village master / cluster planner data¶
The cluster planner reads data/villages.csv (not the Google Sheets). To change it:
- Update the raw SHG workbook
source_data/SHG_Assam_Consolidated_final_Jun22.xlsx(sheet "Village Location Detail"). This workbook is gitignored (PII). -
Regenerate the CSV:
cd leaf_flask python scripts/build_village_master.py --dry-run # preview python scripts/build_village_master.py # write data/villages.csvDistrict/block names are taken verbatim from the survey (uppercased, whitespace-collapsed). The only rows dropped are those with missing/out-of-range coordinates (outside the Assam bounding box).
Missing-coordinate villages silently disappear
Villages whose survey "Location Status = MISSING" have no lat/long and are dropped by the coordinate filter - they cannot be clustered or mapped. They must be given coordinates (or removed) at the source workbook. This bit us on Udalguri.
-
Commit the regenerated
villages.csv, deploy, then run Refresh all clusters on/updateto rebuild coverage (cadre-locked scopes stay protected).
Adding AI policy documents¶
Add PDFs to ai-docs/ and rebuild the vector store (upload via /update → POST /api/config/upload-ai-doc?admin=1, or call the AI init route). The Chroma vector store is persisted at data/vectorstore/.
Files vs. restart¶
Replacing a base file in leaf_flask/data/ requires a service restart to clear the in-memory cache. Google Sheet edits and cluster refreshes do not require a restart.
Production Setup Details¶
Systemd Service (Linux)¶
Create /etc/systemd/system/iwmi-leaf.service:
[Unit]
Description=LEAF DSS Flask Application
After=network.target
[Service]
User=www-data
WorkingDirectory=/opt/iwmi-leaf/leaf_flask
# Secrets: prefer an EnvironmentFile over inline Environment= lines
EnvironmentFile=/opt/iwmi-leaf/leaf_flask/.env
ExecStart=/opt/iwmi-leaf/leaf_flask/venv/bin/gunicorn app:app \
--bind 127.0.0.1:5000 \
--workers 2 \
--timeout 120 \
--preload \
--access-logfile /var/log/iwmi-leaf/access.log \
--error-logfile /var/log/iwmi-leaf/error.log
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
The .env file referenced above must contain at least DATABASE_URL (plus SUPABASE_* for backups and OPENAI_API_KEY for AI).
sudo mkdir -p /var/log/iwmi-leaf
sudo systemctl enable iwmi-leaf
sudo systemctl start iwmi-leaf
sudo systemctl status iwmi-leaf
Nginx Reverse Proxy¶
server {
listen 80;
server_name leaf-asrlm.in;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
# Optional: serve static files directly for better performance
location /static/ {
alias /opt/iwmi-leaf/leaf_flask/static/;
expires 7d;
}
}
Then enable HTTPS with Let's Encrypt:
Docker (Optional)¶
A Dockerfile ships at leaf_flask/Dockerfile (python:3.11-slim + GDAL/GEOS/PROJ). Render uses the native Python runtime path, not Docker, but the Dockerfile is available for Cloud Run / ACI / ECS:
docker build -t iwmi-leaf leaf_flask/
docker run -p 5000:5000 \
-e DATABASE_URL="postgresql://..." \
-e SUPABASE_URL="https://xxxx.supabase.co" \
-e SUPABASE_SECRET_KEY="..." \
-e OPENAI_API_KEY="sk-..." \
iwmi-leaf
Updating the Application¶
Routine Updates¶
cd /opt/iwmi-leaf
git pull origin main
cd leaf_flask
source venv/bin/activate
pip install -r requirements.txt # Only if dependencies changed
python run_migration.py migrations/NNN.sql # Only if a NEW migration was added
cd .. && mkdocs build && cd leaf_flask # Only if docs changed
sudo systemctl restart iwmi-leaf
Check for new migrations before restarting
Deploys do not apply migrations automatically. If a release adds a migrations/NNN_*.sql file, apply it with run_migration.py (it is idempotent) before or right after the restart, or the app will hit a schema it doesn't have. On Render, run migrations once from a local machine or the Render shell against the same DATABASE_URL.
Security Considerations¶
There is NO real authentication - flag this before going more public
LEAF DSS has no login and no user accounts. Every "admin" action (refresh-all, CSV import, dashboard writes, AI-doc upload) is gated only by a ?admin=1 query parameter or X-Admin: 1 header - anyone who knows the URL can call them. This is fine for a trusted internal tool but is a real gap for a public deployment. Add proper authentication (reverse-proxy auth, an API gateway, or app-level auth) before exposing the admin/mutation endpoints to the open internet.
| Area | Current State | Recommendation for Production |
|---|---|---|
| Authentication | None. Admin/mutation endpoints gated only by ?admin=1 / X-Admin:1 - trivially bypassable |
Add real auth (proxy auth, gateway, or app-level) before public exposure. Do not expose POST /api/clusters/refresh-all, /import, dashboard writes, or AI upload without it. |
| Database | Managed Postgres (Supabase) reachable via DATABASE_URL |
Restrict DB network access to the app; rotate credentials on handover; keep the nightly backup running |
| SQL injection | App uses parameterized queries (psycopg2), but there IS a database - not "N/A" | Keep all queries parameterized; never string-format user input into SQL |
| HTTPS | Handled by Render/Nginx | Always use HTTPS in production |
| Secrets | In Render dashboard / .env (gitignored) |
Never commit .env; rotate DATABASE_URL, SUPABASE_SECRET_KEY, OPENAI_API_KEY on handover |
| CORS | Open to all origins | Restrict to your domain if needed |
| Rate limiting | None | Add Flask-Limiter if public-facing |
| File uploads | AI-doc PDF upload + cluster CSV import exist (behind ?admin=1) |
Gate behind real auth; validate content types and size |
| Backups | Nightly to Supabase Storage db-backups (14-day retention) |
Verify restores periodically; add off-Supabase copies for on-prem |