Skip to content

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)

  1. Provision Postgres. Create a Supabase project (or any managed Postgres 14+). Copy the direct connection string into DATABASE_URL.
  2. Create the schema. Apply leaf_flask/schema.sql to create the base tables (clusters, cluster_generation, cluster_villages, infrastructure, maintenance_run).
  3. 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.sql
    

    run_migration.py reads DATABASE_URL from leaf_flask/.env, applies the SQL, and prints a verification line. Migrations 005/006 split the collision-prone LAKHIPUR and BINNAKANDI block 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.

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

  1. Push code to a GitHub repository.
  2. Provision a Supabase project and apply the schema + migrations (see Database Setup).
  3. 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
  4. Add environment variables in the Render dashboard (see Environment Variables): DATABASE_URL, SUPABASE_URL, SUPABASE_SECRET_KEY, SUPABASE_PUBLISHABLE_KEY, OPENAI_API_KEY, optional SECRET_KEY.
  5. Configure the custom domain leaf-asrlm.in under the service's Settings → Custom Domains and point DNS accordingly. (The domain is configured in Render/DNS, not in code.)
  6. Click Deploy. Auto-deploy from main is enabled - every push to main redeploys 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 main branch is what Render auto-deploys
  • [ ] render.yaml reviewed (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 six migrations/*.sql have 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) and JIRA_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 are data/DSS_input2.csv and data/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.csv and data/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 regenerating villages.csv

5. Documentation

  • [ ] This documentation site (docs/, served at /documentation/ from pre-built leaf_flask/site/)
  • [ ] Swagger API docs (auto-generated at /docs)

6. Knowledge Transfer

  • [ ] The /update ops 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, and POST /api/config/refresh to 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:

  1. 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).
  2. Values are cached for 5 minutes; use Refresh on /update (POST /api/config/refresh) to pull immediately.
  3. data/block_values.csv and data/DSS_input2.csv are 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:

  1. Update the raw SHG workbook source_data/SHG_Assam_Consolidated_final_Jun22.xlsx (sheet "Village Location Detail"). This workbook is gitignored (PII).
  2. Regenerate the CSV:

    cd leaf_flask
    python scripts/build_village_master.py --dry-run   # preview
    python scripts/build_village_master.py             # write data/villages.csv
    

    District/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.

  3. Commit the regenerated villages.csv, deploy, then run Refresh all clusters on /update to rebuild coverage (cadre-locked scopes stay protected).

Adding AI policy documents

Add PDFs to ai-docs/ and rebuild the vector store (upload via /updatePOST /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:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d leaf-asrlm.in

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