SUBSTRATE — Sagar Tailor, home

02Current location: Engine level, VAYU-DRISHTI

02engine

CONTRACT 03VAYU-DRISHTI

A satellite air-quality platform for India — surface AQI, formaldehyde hotspots and active-fire monitoring.

01Identification

Identification

My role

Platform layer — API, data layer, observability, container environment

Domain
Geospatial platform
Level
02 · Engine
Stack
FastAPI · PostGIS · Streamlit
Team
4 contributors
Built
July 2026
Sourced statements
14

02Context

Context

The problem

Satellite observations of air quality are not air quality. Turning Sentinel-5P and ERA5 columns into something a person can act on needs a platform: somewhere to put the data, something to serve it, and enough observability to know when an answer is wrong.

Why it matters

The models are the visible half. Without a data layer, a versioned API and request tracing, a four-person team has no way to tell a modelling error from an ingestion error.

03What it does

What it does

A four-person project analysing Sentinel-5P and ERA5 observations to estimate ground-level air quality. Yeshika owns the machine-learning pipeline; Soumyadeb owns the AQI calculation and Earth Engine ingestion.

I own the platform: the API, the data layer, the observability, the container environment, and the dashboard the team's models are seen through. Three commits, roughly 11,700 lines across 116 files — the substrate everything else runs on.

And where that is proven

Configuration refuses to boot the server on DEBUG=True with ENVIRONMENT=production. The misconfiguration raises at import time, before the application accepts a single request.
backend/app/core/config.pyVerify: open backend/app/core/config.py on GitHub in a new tab
Request-ID middleware binds a UUID into structlog contextvars, so every log line emitted during a request carries the same trace ID — and the ID returns to the caller on the X-Request-ID header.
backend/app/main.pyVerify: open backend/app/main.py on GitHub in a new tab
Alembic runs migrations through asyncio.run() and create_async_engine(), so asyncpg is the only PostgreSQL driver in the project. psycopg2 is deliberately absent rather than carried as a second dependency.
backend/requirements.txtVerify: open backend/requirements.txt on GitHub in a new tab
The DATABASE_URL is a computed field assembled from separate components with quote_plus encoding, so special characters in credentials cannot corrupt the connection string.
backend/app/core/config.pyVerify: open backend/app/core/config.py on GitHub in a new tab

04Architecture

Architecture

A FastAPI application factory over an async PostGIS data layer, with configuration validated and logging configured before anything else is constructed. The models and the AQI calculation sit on top of this and are not mine. The platform underneath them is.

Layers, shallow to deep

  1. Streamlit dashboardGIS map, charts and report pages — the surface the team's models are actually seen through.
  2. Versioned APIAn /api/v1 router with Pydantic schemas and a service layer, mounted by the application factory rather than by import side effect.
  3. MiddlewareCORS from configuration, and a request-ID that binds a UUID into structlog contextvars and returns to the caller on X-Request-ID.
  4. ConfigurationPydantic settings validated at import, cached for the process lifetime, with the database URL assembled from separate components.
  5. Async data layerSQLAlchemy over asyncpg, with Alembic running migrations through asyncio.run() so there is only ever one PostgreSQL driver.
  6. Container environmentThe API and its database, defined so the rest of the team can run the platform without configuring it.

What happens to one request

  1. Startup

    Settings load and logging is configured before any logger exists.

  2. Request-ID

    A UUID is bound into the logging context for this request.

  3. CORS

    Origins from configuration; restricted in production.

  4. v1 router

    Versioned route handlers, typed in and typed out.

  5. Service

    Business logic, isolated from transport.

  6. Async session

    A connection from the async engine, released with the request.

  7. Response

    The trace id returns on X-Request-ID.

backend/app/main.pyVerify: open backend/app/main.py on GitHub in a new tab

05Decisions

Decisions

Each one states what forced it, what was rejected, the reasoning, and what it cost.

  1. 01Decision

    Inject a UUID per request in middleware and bind it into structlog contextvars, returning it on X-Request-ID.

    over Per-module logging with no correlation identifier.

    The pressure
    Four people are changing four layers, and a log line that cannot be tied to the request that produced it cannot separate a modelling error from an ingestion error.
    Why this way
    Binding into contextvars means every line emitted anywhere during that request carries the id without a single call site passing it — through middleware, handlers and services alike. Returning it on the header means the caller can quote the id for their own failed request.
    What it cost and bought
    Full request tracing costs one middleware and one header, with no tracing backend to run.
  2. 02Decision

    Validate settings at import time, and raise on DEBUG=True with ENVIRONMENT=production in a model validator.

    over Reading environment variables where they are needed and checking at use time.

    The pressure
    A misconfigured deployment is most dangerous when it starts successfully.
    Why this way
    The failure has to happen before the application accepts a request. A check at the point of use fires after the thing it was guarding has already been exposed.
    What it cost and bought
    A bad environment is a startup crash naming the field, rather than a production server running with debug behaviour and interactive docs exposed.
  3. 03Decision

    Derive it as a computed field from separate components, encoding user and password with quote_plus.

    over Accepting a raw connection string from the environment.

    The pressure
    A database URL is a single opaque string containing credentials, and it gets logged, pasted and copied.
    Why this way
    Special characters in a password corrupt a hand-assembled URL, and a single opaque variable makes accidental exposure the easy path rather than the careless one.
    What it cost and bought
    Credentials are ordinary separate settings; the connection string is derived and never authored by hand.
  4. 04Decision

    Run migrations through asyncio.run() with an async engine, keeping asyncpg as the only driver.

    over Installing psycopg2 alongside asyncpg for migrations.

    The pressure
    Alembic conventionally wants a synchronous driver, which would put a second PostgreSQL driver into a project that is otherwise entirely async.
    Why this way
    Two drivers means two connection behaviours, two sets of type adapters and a second thing to keep configured — for a task that runs a handful of times.
    What it cost and bought
    One driver and one URL. Migrations execute on the same stack the application runs on, so a migration that works is evidence the application's connection settings work.
  5. 05Decision

    Serve /docs and /redoc in development and staging, and disable them in production.

    over Leaving them enabled everywhere because they are useful.

    The pressure
    Interactive API documentation is the fastest way for a team to learn an API and an inventory of the surface for anyone else.
    Why this way
    The people who need them are not in production, and the environment already knows which one it is — so the decision can be made by configuration rather than by remembering.
    What it cost and bought
    The team keeps the documentation; production does not publish its own route inventory.

06Challenges

Challenges

  • The constraint

    Three of the four people on the project never touch the platform, and their work has to attach to it without editing it.

    What the system does about it

    An application factory rather than a module-level app, a versioned router, and typed schemas at the boundary — so a model or an endpoint is added by registration, and tests can build an application with custom settings without affecting any other test.

  • The constraint

    Observability has an ordering problem: any logger created before logging is configured is configured wrongly, and will be for the life of the process.

    What the system does about it

    Settings load and logging is configured as the first module-level actions in the entry point, before the factory runs and before any logger is created.

07Results

Results

Lines, platform layer
11,700
Across 116 files
API endpoint modules
6
Pydantic schema modules
7
Commits
3
Plus repo initialisation

08Attribution

Attribution

Four contributors. I own the platform everything else runs on; the machine learning and the AQI calculation are not mine.

ML pipeline — models, feature selection, evaluationyeshika-0226 commits
AQI calculation · Earth Engine ingestion · Random Forestsoumyadeb10 commits
Streamlit dashboard · GIS map · charts · report pagesSagar1,854 lines
API v1 · schemas · services · live-data hardeningSagar2,599 lines
Config · structured logging · async data layer · Docker · testsSagar6,731 lines

Not mine:The machine-learning pipeline is Yeshika's and the AQI calculation and Earth Engine ingestion are Soumyadeb's. I did not build the models.

Built with

09Next iteration

Next iteration

Each of these is something the repository already records as unfinished, not a feature list. Where a project states no such thing, this section is absent rather than invented.

  1. 01Open

    Fill the lifespan hook: database pool warm-up, model loading and background task startup.

    The lifespan context manager is in place and the code says that is where those belong, but it does nothing yet — so the first request after a deploy currently pays for a cold pool.

Still to write

Still to write

9 of 10 sections written and sourced

The rest are absent rather than filled with plausible prose, which is the whole point: inventing them would cost exactly the credibility the rest of this page is built to earn.

  • LessonsNot written yet
What these mean
Key to the states above.
Not written yet
Real and known, but not written up.

Navigation

Use arrow keys to select, Enter to open, Escape to close.