
The Street That Floods in July and Runs Dry in May: Building INDRA on Open-Source AWS Tools
INDRA- Initiative for Drainage and Rainwater Acquisition Built for the AWS Bharat Builds environmental hackathon at Delhi Technological University (DTU)
Picture a lane in an Indian city in July. The drain is blocked, the water is knee deep, and nobody is going anywhere. Now picture the same lane in May. Same houses, same families. The tap runs for forty minutes before sunrise, if it runs at all, and the tanker has quietly raised its price again.
Those two scenes are one problem seen in different months. We are bad at holding on to water when there is too much of it, so we run short when there is too little.
INDRA is our answer, built one rooftop at a time. The name stands for Initiative for Drainage and Rainwater Acquisition. Drainage is about getting monsoon water off the street without a fuss. Acquisition is about catching some of it so it's still there when summer arrives. The people who use it are called Droplets. Yes, that's a bit cheesy. We kept it.
The hackathon brief was one line: too much water, too little of it, and the heat in between. It also asked us to build with the open-source AWS tools. We took that literally, and what came out is a working platform, not a mock-up. Under the hood there's a climate atlas for 754 districts (roughly 19,000 pincodes), a Python API, a React front end, a small Java service for tank sensors, a serverless alert pipeline and an AI assistant that cites where its answers come from. All of it runs on a laptop. No AWS account, no bill.
This post is a tour. Most of it covers the standard, urban side of INDRA, the part meant for households and housing societies, and I'll point out where each AWS open-source tool earned its place.
Two front doors, one engine
INDRA has two modes. Standard is for apartment owners, independent houses and housing societies. INDRA-Gramin is for farmers, cattle owners and village councils. One toggle in the navbar flips the whole app: pages, navigation, colours (blue for standard, green for Gramin) and even the personality of the chatbot. The mode lives in the URL, so a shared /gramin/ link opens where it should.

We split them because the weather is shared and the questions aren't. An apartment owner in Mumbai wants a quote and a tank drawing, plus some idea of what to do about the drain outside the gate. A farmer in Rajasthan wants to know how many buckets the cattle get this week and whether bajra makes more sense than rice. Squeezing both into one interface would have made it worse for each, so there is one climate core underneath and two doors on top.

The rest of this post stays with the standard door.
A household's walk through INDRA
Start with a pincode. You enter a pincode or pick a district and INDRA shows three gauges from 0 to 100: flood risk, water scarcity and heat. Each gauge has a chip (Low, Moderate, High or Severe) and, more usefully, the reasons behind the score. Seasons are described the way people feel them. Pre-monsoon reads as "the tap runs dry", not as a rainfall deficit.

Below the gauges sits a live seven-day forecast tagged with IMD rainfall classes: heavy, very heavy or extremely heavy. Every alert carries a "What to do" line, because a warning with no instruction is just anxiety. For example: clear the first-flush chamber and open the overflow towards the recharge pit.
Then the roof. The assessment page asks for things you already know: roof area, roof type, budget and location. It returns a recommended system (rooftop tank, recharge pit or a hybrid), a tank size, a cost breakdown and a feasibility score. It also reports one number we added on purpose: the litres you keep off the street each monsoon.

The tool knows physical limits too. A thatch or asbestos roof can't carry a rooftop tank, so INDRA won't pretend it can. On a flood-prone plot it leans towards recharge-pit logic, because a bigger tank is no answer to a street that is already underwater.
See how it works. The 3D visualizer, built with Three.js and React Three Fiber, walks through a rainwater harvesting system part by part: catchment, gutters, first flush, filter, tank. It is lazy-loaded so the first page stays quick. The idea is simple. A system is easier to trust when you can see how the parts fit together.

Find someone to build it. Vendor Connect lists vendors and comes with a DIY installation guide. One caveat, stated in the app itself: the vendor contacts are starter data and we have not verified them. Where we have nothing for a city, INDRA gives honest search links instead of inventing businesses.
Watch the tank. Register a tank with a sensor and the monitor tells you things like "overflow in 2 hours" during a cloudburst or "empty in 14 hours" in a heatwave. More on how that works further down.
Hear about trouble early. Subscribe a district and a scheduled function scores it every three hours, then sends new alerts by email or SMS.
Ask a question. The assistant is a Strands agent. In standard mode it talks about costs, payback, tanks, filtration and flood-proofing. When it needs a number it calls a tool instead of guessing.
That's the loop: understand the risk, size the system, watch it work, get warned in time.
The numbers, out in the open
Water advice that can't show its working is hard to trust, so the formulas are simple on purpose and the shared code is plain standard-library Python.
The flood score looks at main-season rainfall and how concentrated it is, then adds a live bump from the forecast using the IMD classes: heavy at 64.5 mm in 24 hours, very heavy at 115.6, extremely heavy at 204.5. "Main season" means whichever monsoon is wetter, south-west or north-east. That detail exists because of Chennai, which gets most of its rain from the north-east monsoon and would be badly under-scored if we only looked at June to September.
The scarcity score is half low annual rainfall and half groundwater stress, using CGWB extraction figures. The heat score uses the forecast peak temperature when we're online. Offline, it falls back to an estimate that is labelled as an estimate.
Then there is the swing index, the idea the whole project hangs on: min(flood, max(scarcity, heat)). It is high only where both extremes bite. These are the flood-and-famine districts, where one rooftop system has to do two jobs. A tool that only tracks flooding would miss them, and so would one that only tracks scarcity.
Finally the water balance: harvest equals roof area × rainfall × runoff coefficient. One millimetre of rain on one square metre is one litre, which keeps the arithmetic honest. Take a 100 m² RCC roof in a district with 1,000 mm of annual rain. That harvests roughly 80,000 litres (100 × 1,000 × 0.8). A four-person household using 135 litres per person per day goes through 540 litres daily, so the harvest covers about 148 days. Every litre in that tank is also a litre that never reached the street.

The dataset problem that made us label everything
The earlier "processed" version of our climate data had a problem. Its rainfall and groundwater columns had been z-score normalised, which means some values were negative, and missing rainfall had been filled with the global median. A correction table existed to paper over the damage. A district showing negative rainfall at least announces that something is wrong. A district showing a plausible but wrong number, quietly filled in from a global median, announces nothing, and that second kind is the more dangerous one.
We dropped it and rebuilt the atlas from the raw survey. Of the 754 districts, 298 have observed values and 27 carry IMD overrides. Another 414 are inferred from the three nearest observed districts (inverse-distance weighted, up to 450 km), and 15 in places like J&K, Ladakh and the Andamans come from regional climatology. Every API response says which of those a number came from, through fields like rainfall.source and data_confidence.
If a score is a guess, the app says it's a guess. That one decision shaped almost everything we built afterwards.
Cleaning up after ourselves
Hackathon code piles up. By the time we stopped adding features, the repo held overlapping backend and front-end folders and three identical copies of the same 88 MB CSV. Six scattered READMEs didn't help. A good part of the work was deleting things.
The duplicate folders went first, and the extra CSV copies moved out of the repo. Then came the bigger cuts. Our AI layer had been wired up in five separate places, with Qdrant and LangChain in the mix, and we replaced all of it with one. We removed a set of platform statistics and news links because the numbers weren't real, and a dashboard full of impressive totals only stays impressive until somebody asks where they came from. A few GET endpoints that changed data became proper write routes. That shortcut works in a demo and is a bad habit everywhere else.
The front end got the same treatment. One central API client replaced the hard-coded URLs, the route and mode bugs that forced full-page reloads were fixed, and an error boundary now stops one broken page from blanking the whole app.
None of that adds a feature you can demo. It's the reason the project now starts with two commands, and the reason every number on screen is one we can explain.
Rule one: the model never owns a number
Cost, feasibility, tank dimensions, water budgets and every risk score are computed by deterministic code. A language model may add narrative, a plan or friendlier wording, and that's all.
The payoff is practical. When the model is unreachable the API still returns a complete answer, flagged ai_enhanced: false. The assistant degrades in steps: the Strands agent with its tools first, then plain retrieval-augmented answers, then an offline answer grounded in our document library. Retries with backoff and a circuit breaker keep a dead provider from stalling requests. For a tool someone might open on a patchy connection during a flood, that matters more than model quality.
The library behind it holds 16 reference documents (the CPWD manual, IS codes, handbooks and crop guides) split into 2,447 chunks, embedded with MiniLM and stored in OpenSearch for k-NN search. If the cluster is down or empty, a local index takes over automatically.
The AWS open-source toolkit, tool by tool
The Strands Agents SDK runs the assistant's agent loop. The agent has three tools: climate risk, roof sizing and cited document search. It works with any OpenAI-compatible model, so OpenRouter's free models or a local Ollama both do the job. Our tests run the real agent loop against a fake streaming LLM, including the tool-call round trip and each failure fallback, which means no network is needed to check it.
OpenSearch is the vector store for the document library, using k-NN with HNSW and cosine similarity. Its outage and error fallbacks are tested with fakes.
Cedar makes every authorization decision. It gets its own section below.
Amazon Corretto is the JDK underneath the tank-telemetry service.
The AWS SAM CLI defines the scheduled Lambda, the DynamoDB tables and the SNS topic in a single template.yaml. LocalStack, the local AWS emulator, then gives us DynamoDB, SNS, SES and Lambda on a laptop. A bootstrap script builds the tables straight from the SAM template, so the template stays the single source of truth.
Finch builds and runs the container images and the compose stack. EKS Distro and EKS Anywhere carry the Kubernetes side: a kustomize base with an overlay, probes, resource limits, an autoscaler, a PodDisruptionBudget, default-deny NetworkPolicies and non-root containers.
Firecracker boots the telemetry service inside a microVM. The reason is in the next section.
PartyRock is hosted-only and can't be called from code, so we used it where it's strongest. Two no-code blueprints (a monsoon-ready household and a heatwave water budget) are there for quick prototyping, while the production logic lives in the Strands agent.
Tank telemetry: a small Java service with a big wall around it
The tank monitor is a Java 21 service with zero third-party dependencies. It runs on Corretto and uses the JDK's built-in HttpServer with virtual threads. Each tank gets its own device key, and we store only the SHA-256 hash of it. As level readings stream in, the service fits a regression over the last three hours and estimates hours until empty or hours until full. Snapshots are written atomically, so tanks survive a restart.
This is the one component that accepts traffic from hardware out in the world, so we wanted it walled off. The Firecracker setup (a rootfs builder, a VM config and a run script) boots it in a microVM, which isolates untrusted sensor traffic at the hardware level. It needs Linux with KVM, and I'll come back to that in the honesty section.
You don't need a sensor to see it work. scripts/simulate_tank.py streams a cloudburst followed by a heatwave dry spell into a fresh tank, and watching the warning change from overflow to running dry makes the whole idea click.
Alerts without a server to babysit
An EventBridge schedule fires a Lambda every three hours. The Lambda evaluates every subscribed district, de-duplicates against what has already gone out, stores new alerts in DynamoDB, publishes to SNS and delivers per subscriber: email through SES, SMS through SNS.
It imports the same indra_climate package as the API. That package is pure standard-library Python, so the score on the website and the score in your inbox cannot disagree.
Policy as code with Cedar
Who can read a tank? Who may publish an alert? Scattered role checks inside route handlers are where authorization bugs like to live, so all of our rules sit in one Cedar file, default-deny. Owners manage their own tanks and subscriptions. Leaders and admins publish alerts. Only admins retrain models, and a suspended account is refused even if it belongs to an admin.
More than twenty policy and API tests sit around that file. For local work there is a demo auth mode, and the app refuses to start with it when INDRA_ENV=production.
Making it boring to operate
Boring is a compliment here. Every request gets an ID, logs are structured, and errors come back in one shape no matter what failed. There's rate limiting, plus /health, /ready and /api/status for anything that wants to poke at the service. The test suite has 135 tests: 20 for the climate engine, 80 for the backend, 13 for the serverless evaluator and 22 for the Java service. On top of that sit frontend typechecking and linting, a deployment-manifest validator and a CI workflow. All of it runs offline, so anyone can clone the repo and see the same green we saw.
What we haven't verified
Our development machine had no container engine, no sam CLI, no KVM and no Kubernetes. So the container images, the cluster apply, sam build and the Firecracker boot are validated statically (linting, manifest validation, cfn-lint) but have not been executed. What those tools consume is tested in-process: the Lambda against moto, the agent against a fake LLM, the Java service through its real HTTP API.

I would rather say that plainly than have someone find it later. The same goes for the data. Groundwater figures are state-level in the source survey, heat is a forecast or a labelled estimate, the dry-spell length behind tank sizing is a heuristic, and two scanned reference PDFs contribute no text without OCR. The app states these limits where they matter.
Where it goes next
The biggest step is real sensors at scale: low-cost ultrasonic level sensors and rain gauges over LoRa or GSM, feeding the telemetry service, with readings kept in DynamoDB or Timestream. That would move INDRA from estimated water to measured water and make neighbourhood-level flood and shortage maps possible.
Forecasts can get finer, with nowcasting, IMD district warnings and ward-level flood models. CGWB district well data would replace the state-level groundwater figures and the heat estimate with measured values.
A plan only matters if it gets built, so a verified vendor network and scheme checks for Jal Shakti Abhiyan, Atal Bhujal and state subsidies would help turn recommendations into installed, funded systems. On the reach side, WhatsApp, IVR and voice in more Indian languages would bring in people who never open a web app. Production hardening comes last on the list: a multi-replica telemetry store, managed OpenSearch with TLS, and SES and SNS in a real AWS account.
Try it yourself
You need Python 3.12 and Node 22. A JDK 21 or newer, such as Amazon Corretto, is optional and only needed for tank monitoring.
python scripts/dev.py setup
python scripts/dev.py up
Open localhost:5173 and it works with no keys at all, using the deterministic engines and offline answers. To switch on LLM enrichment, set LLM_API_KEY or point LLM_BASE_URL at a local Ollama. Prefer containers? finch compose up --build starts the web app, the API, telemetry and OpenSearch.
Why this matters
Floods and shortages look like opposites, but they come from the same gap: we don't capture water when there's too much and we don't ration it when there's too little. INDRA tries to give every household an early local warning, a right-sized system to catch and recharge monsoon water, a plan for using it through the summer, and a community to share it with. Because the whole stack costs nothing to run, the benefit doesn't depend on who can afford a cloud bill.
If you've dealt with waterlogged streets or tanker prices in your own city, I'd like to hear what you'd want a tool like this to do. And if you build something on top of INDRA, tell us.
Built by Sudhanshu Shekhar, Soumya Sourav Das, Palak Jain and Suravi Jha at DTU.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article