SAANS — Smog Advisory & School Closure Decision System
00 — Project Overview & System Architecture
Read this file first. This is the master spec. Files 01–04 in this folder are phase-by-phase build instructions that assume you have read this document. Build in the order: 00 → 01 → 02 → 03 → 04.
Code review notice (applies to every file in this set): All code produced from these instructions will be reviewed by Claude Code before it is accepted into the repo. Write clean, modular, well-commented, efficient code from the start:
- No dead code, no commented-out blocks, no placeholder TODOs left unexplained.
- Small, single-responsibility functions/modules — avoid 300-line files.
- Consistent naming, typed where the language supports it (TypeScript types / Python type hints / Pydantic models).
- Every API endpoint documented (docstring or OpenAPI annotation) with inputs, outputs, and error cases.
- No hardcoded secrets/API keys — use
.env+.env.example.- Include basic error handling and input validation on every external call (AQI APIs, WhatsApp, Twilio, DB).
- Write code assuming a stricter reviewer will read every line and reject sloppy work — optimize for clarity over cleverness.
1. Project Identity
- Name: SAANS ("breath" in Urdu)
- Goal: Give individual Lahore schools a daily, location-specific smog risk score (Green/Amber/Red) and a concrete recommended action, distributed via web, WhatsApp, and printable PDF — replacing blanket province-wide closure orders with targeted, timely, explainable guidance.
- Hackathon constraint: Must be fully buildable and demoable within ~36 hours. Every architectural choice below is deliberately the simplest thing that works, not the most sophisticated thing possible.
2. High-Level System Architecture
┌─────────────────────────────┐
│ External Data Sources │
│ - WAQI / aqicn.org API │
│ - OpenAQ v3 API │
│ - Open-Meteo (wind/forecast) │
│ - OSM Overpass (school seed) │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ SCORING ENGINE (cron) │
│ - nearest-station lookup │
│ - interpolation fallback │
│ - wind/time-of-day adjustment │
│ - tier classification │
│ - confidence indicator │
└───────────────┬───────────────┘
│ writes
┌───────────────▼───────────────┐
│ DATABASE (Postgres/ │
│ SQLite for demo) │
│ schools | readings | scores │
│ subscribers | broadcast_log │
└───────────────┬───────────────┘
│ reads
┌──────────────────────────┼──────────────────────────┐
│ │ │
┌──────────▼─────────┐ ┌────────────▼───────────┐ ┌──────────▼─────────┐
│ BACKEND API │ │ PDF BULLETIN JOB │ │ WHATSAPP/SMS BOT │
│ FastAPI or Node/ │ │ headless HTML→PDF │ │ WhatsApp Business │
│ Express (REST) │ │ (Puppeteer) │ │ API sandbox / │
│ │ │ │ │ Twilio │
└──────────┬───────────┘ └────────────┬────────────┘ └──────────┬─────────┘
│ │ │
┌──────────▼───────────┐ │ │
│ WEB DASHBOARD │ │ │
│ React + Leaflet map │ │ │
│ search + trend view │ │ │
└────────────────────────┘ ┌───────────▼──────────┐ ┌───────────▼─────────┐
│ Posted at school gate │ │ Parent/school │
│ (printable PDF) │ │ WhatsApp groups │
└─────────────────────────┘ └─────────────────────┘
3. Core Components (map to Phase files)
| # | Component | Phase file | Owner role (from proposal) |
|---|---|---|---|
| 1 | Data ingestion + Scoring engine + DB schema | 01_PHASE1_BACKEND_DATA_SCORING.md |
Data/backend engineer |
| 2 | Web dashboard (search, map, trend) | 02_PHASE2_FRONTEND_DASHBOARD.md |
Frontend engineer |
| 3 | WhatsApp bot, SMS fallback, PDF bulletin generator | 03_PHASE3_DISTRIBUTION_WHATSAPP_PDF_SMS.md |
Integrations engineer |
| 4 | Timeline, milestones, demo script, pitch deck | 04_MILESTONES_TIMELINE_TEAM.md |
Product/pitch lead |
4. Technology Stack (final decisions — do not re-litigate mid-build)
| Layer | Choice | Why |
|---|---|---|
| Backend API | Python + FastAPI (preferred) or Node/Express if team is stronger in JS | Async-friendly, auto OpenAPI docs, fast to scaffold |
| Database | SQLite for hackathon demo, schema written to be Postgres-compatible | Zero setup, portable, upgrade path is trivial |
| ORM | SQLAlchemy (Python) or Prisma (Node) | Type-safe queries, easy migrations |
| Scheduler | APScheduler (Python) or node-cron (Node) | In-process cron, no external infra needed |
| Frontend | React + Vite + TypeScript, Leaflet.js for map, TailwindCSS for styling | Fast dev loop, small bundle, good map ecosystem |
| PDF generation | Puppeteer (headless Chrome) rendering an HTML template | Reuses dashboard's HTML/CSS, no separate PDF templating language |
| WhatsApp Business Platform (Cloud API) sandbox | Free tier, official, well-documented | |
| SMS fallback | Twilio | Simple REST API, generous trial credit |
| Hosting (demo) | Render / Railway / Fly.io for backend, Vercel/Netlify for frontend | Free tiers, fast deploy, good for a 36-hr build |
| Version control | GitHub, one repo, trunk-based with short-lived feature branches | Required deliverable is a GitHub repo |
5. Repository Structure (create this exact skeleton on day 0)
saans/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app entrypoint
│ │ ├── config.py # env var loading
│ │ ├── db/
│ │ │ ├── models.py # SQLAlchemy models
│ │ │ └── session.py
│ │ ├── ingestion/
│ │ │ ├── waqi_client.py
│ │ │ ├── openaq_client.py
│ │ │ └── openmeteo_client.py
│ │ ├── scoring/
│ │ │ ├── engine.py # core scoring logic
│ │ │ ├── tiers.py # tier thresholds + labels
│ │ │ └── confidence.py # distance-based confidence calc
│ │ ├── schools/
│ │ │ ├── registry_loader.py # Overpass ingestion + manual seed merge
│ │ │ └── seed_schools.json
│ │ ├── api/
│ │ │ ├── routes_schools.py
│ │ │ ├── routes_scores.py
│ │ │ └── routes_subscribers.py
│ │ └── jobs/
│ │ └── scheduler.py # 6 AM + midday refresh jobs
│ ├── tests/
│ ├── requirements.txt
│ └── .env.example
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── MapView.tsx
│ │ │ ├── SchoolSearch.tsx
│ │ │ ├── ScoreCard.tsx
│ │ │ └── TrendChart.tsx
│ │ ├── api/client.ts
│ │ ├── pages/Dashboard.tsx
│ │ └── main.tsx
│ ├── package.json
│ └── .env.example
├── distribution/
│ ├── whatsapp/
│ │ ├── bot.py # webhook + broadcast sender
│ │ └── message_templates.py
│ ├── sms/
│ │ └── twilio_sender.py
│ └── pdf/
│ ├── bulletin_template.html
│ └── generate_bulletin.py # Puppeteer/pyppeteer wrapper
├── docs/
│ ├── ARCHITECTURE.md # copy of this file
│ ├── API_CONTRACT.md
│ └── demo_script.md
├── .github/workflows/ci.yml # lint + test on push
└── README.md
6. Data Model (authoritative — implement exactly this in Phase 1)
schools
| field | type | notes |
|---|---|---|
| id | UUID/int PK | |
| name | string | |
| zone | string | e.g. DHA, Model Town, Gulberg, Township, Batapur-adjacent |
| lat, lon | float | |
| source | enum(overpass,manual) | |
aqi_readings
| field | type | notes |
|---|---|---|
| id | PK | |
| station_id | string | from WAQI/OpenAQ |
| lat, lon | float | station location |
| aqi_value | int | |
| pm25 | float nullable | |
| recorded_at | datetime | |
| source | enum(waqi,openaq) | |
scores
| field | type | notes |
|---|---|---|
| id | PK | |
| school_id | FK → schools | |
| score_date | date | |
| computed_at | datetime | |
| raw_aqi | float | interpolated or nearest-station value |
| adjusted_aqi | float | after wind/time-of-day adjustment |
| tier | enum(green,amber,red) | |
| recommendation | string | human-readable action text |
| confidence | enum(high,medium,low) | based on distance to nearest station |
| distance_to_station_km | float | |
subscribers
| field | type | notes |
|---|---|---|
| id | PK | |
| school_id | FK → schools nullable | null = neighbourhood-level subscriber |
| channel | enum(whatsapp,sms) | |
| contact | string | phone number |
| opted_in_at | datetime | |
broadcast_log
| field | type | notes |
|---|---|---|
| id | PK | |
| subscriber_id | FK | |
| score_id | FK | |
| sent_at | datetime | |
| status | enum(sent,failed) | |
7. Tier Logic (must match proposal exactly)
| Tier | AQI Range | Action |
|---|---|---|
| Green — Normal | 0–100 | Outdoor activity, sports, recess proceed as normal |
| Amber — Caution | 101–200 | Move recess/sports indoors; sensitive students avoid outdoor exposure |
| Red — High Risk | 201+ | Recommend remote learning or indoor-only day; flag to admin for closure decision |
Implement thresholds as named constants in scoring/tiers.py, not magic numbers scattered across the codebase.
8. Non-Negotiable Constraints for the Build
- Every AQI/weather API call must have a timeout, retry-once, and graceful fallback (use last-known-good cached reading rather than crashing).
- The system must never present an interpolated score as if it were a direct sensor reading — the
confidencefield is mandatory on every score object returned by the API. - No feature (WhatsApp, SMS, PDF) should block the core deliverable (web dashboard with real scores). Build in the priority order defined in file
04_MILESTONES_TIMELINE_TEAM.md. - All official/health-authority language must stay hedged: SAANS is a decision-support aid, not a replacement for Punjab EPA/health authority guidance. This applies to UI copy, WhatsApp message templates, and the PDF bulletin footer.
9. Environment Variables (define in .env.example at repo root and per-service)
WAQI_API_TOKEN=
OPENAQ_API_KEY=
OPENMETEO_BASE_URL=https://api.open-meteo.com/v1
DATABASE_URL=sqlite:///./saans.db
WHATSAPP_CLOUD_API_TOKEN=
WHATSAPP_PHONE_NUMBER_ID=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
FRONTEND_BASE_URL=http://localhost:5173
BACKEND_BASE_URL=http://localhost:8000
Next: proceed to 01_PHASE1_BACKEND_DATA_SCORING.md.

Log in or sign up for Devpost to join the conversation.