
Rechargematch: Building a Physics-Aware Urban Groundwater Recharge Engine on Serverless AWS
How I combined terrain-constrained gravity routing, Central Ground Water Board (CGWB) soil percolation mechanics, and a scale-to-zero AWS serverless stack (API Gateway, Lambda, DynamoDB, S3, and CloudFormation) to turn urban monsoon waterlogging into aquifer recharge.
Every July, the Moolchand Underpass and Ring Road corridor in South Delhi goes under waist-deep water. Traffic stalls for hours.
Ten months later, in May, the same neighborhood is queuing for private water tankers because the borewells have run dry.
I'm talking about South Delhi Ward 142 (Defence Colony and Lajpat Nagar). The Central Ground Water Board (CGWB) puts groundwater extraction there at 124.3%, which is classed as Over-Exploited. Yet more than 792 mm of rain falls on the city every monsoon. It runs off the asphalt, overwhelms the storm drains, and ends up in the Yamuna without ever touching the aquifer.
Indian cities don't really have a water scarcity problem. They have a water routing failure.
Municipalities already keep lists of their worst waterlogging hotspots. What engineers and Resident Welfare Associations (RWAs) don't have is a planning tool that answers one hard question:
"Given a flooded intersection, which nearby parks, school playfields, or municipal grounds can actually receive that water by gravity, how much can the soil absorb in 48 hours, and how much runoff is still left over?"
So I built RechargeMatch, a ward-level stormwater routing and groundwater recharge siting engine. The front end is React 19, TypeScript, and Google Maps. The back end is fully serverless on AWS.
This post covers two things: why the physics engine works the way it does, and how I built the AWS side to cost almost nothing when it's idle.
Why existing siting tools fail in the field
I talked to drainage engineers and looked at earlier GIS tools. Three problems kept coming up.
1. The uphill pit. Most tools match a flooded intersection to the nearest park by straight-line distance. Moolchand Underpass sits at
210.2 m MSL . The ground around Moolchand Hospital is only 400 m away, but it's at 211.8 m MSL , which is 1.6 m uphill. Water won't go there without a diesel pump, and those pumps tend to fail when the power cuts out in a storm.2. Ignoring soil. A
300 m³ recharge trench doesn't capture 300 m³ per storm if silt-loam soil can only percolate 96 m³ before the 48-hour mosquito-breeding window closes. The trench is big, but the soil is the real limit.3. Contaminating the aquifer. Several chronic flood spots in Ward 142 sit next to Kushak Nala, an unlined storm-sewage channel. Send sewage-laced street backflow down a recharge shaft and you've injected pathogens straight into the drinking-water table.
The physics engine
Before writing any UI code, I built a deterministic matching engine (
matchingEngine.ts ) around four rules I wasn't willing to bend.1. Gravity decides what's reachable
For every waterlogging hotspot
H and candidate open space S , I compare surface elevations from Digital Elevation Model (DEM) benchmarks, with a 0.3 m noise tolerance:text
Elevation(Site) <= Elevation(Hotspot) + 0.3 m
Uphill sites are rejected immediately. Each rejection goes into an audit trail with its exact elevation difference, so an engineer can see why a site was dropped.
2. Scoring the downhill candidates
Sites that pass the gravity test and sit within a
1,600 m street-drain flow path (a 1.25x urban tortuosity factor) get a score from 0 to 100 across six weighted factors:- Flow-path distance (25%): longer trenching along street drains scores worse.
- Usable open storage area (20%): bigger parks and institutional playfields score better.
- Soil hydraulic conductivity (20%): Yamuna Older Alluvium classes, from Sandy Loam (
26 mm/h) down to Clay Loam (6 mm/h).
- Vadose zone depth (15%): shallow water tables (
< 3.0 mbelow ground) are heavily penalized.
- Flood exposure (10%): low-lying sinks that flood in the same storm are excluded.
- Land use and access (10%): MCD/DDA public parks and government schools beat private parcels.
3. Honest volume accounting
For each hotspot and storm scenario, from the standard
60 mm CPWD design storm up to the 228 mm Delhi deluge of June 28, 2024, RechargeMatch calculates low , base , and high bounds for five equations:text
1. Upstream sheet runoff : V_runoff = C × P × A2. Physical tank storage : V_store = L × W × D × void_ratio3. 48-hour soil percolation : V_drain = f × A_floor × 48 hours4. Structure design capacity : V_design = min(V_store, V_drain)5. Ward absorption gap : V_gap = V_runoff - Σ V_captured
Small decentralized pits can't swallow a cloudburst, and I didn't want the tool to pretend otherwise. So the headline output is the Ward Absorption Gap (
V_gap ), the volume that's still left over. That's the number engineers need for trunk sewer desilting.4. Safety gates
- Sewage gate: any hotspot next to Kushak Nala (
hs-2,hs-10) is automatically downgraded from Aquifer Recharge toDETENTION ONLY, with0 m³of direct groundwater injection. The tool recommends a vegetated bio-detention basin instead.
- Silt trap and first-flush gate: every structure spec includes a dual-chamber desilting interceptor, so road grit is trapped before water reaches the gravel trench.
The AWS architecture
A static model goes stale. I wanted engineers, RWAs, and ordinary citizens to report live waterlogging depth, drain time, and geotagged photos during the monsoon.
That traffic is very uneven. It spikes for a few hours during a cloudburst, then drops to almost nothing for weeks. Paying for always-on EC2 instances or an RDS database to serve that pattern made no sense for a civic project, so I went scale-to-zero serverless, with everything defined as code in AWS CloudFormation.
text
┌────────────────────────────────────┐│ RechargeMatch Web Planner ││ (React 19 + TypeScript + GisMap) │└─────────┬──────────────────┬───────┘│ 1. REST JSON │ 3. Direct PUT (Pre-Signed URL)▼ │┌─────────────────────────┐ ││ Amazon API Gateway │ ││ (HTTP API v2) │ │└─────────┬───────────────┘ ││ 2. Invoke │▼ ▼┌─────────────────────────┐ ┌──────────────────────────────┐│ AWS Lambda │ │ Amazon S3 ││ (Node.js 20.x Handler) │ │ (Sanitized Evidence Bucket) │└─────────┬───────────────┘ └──────────────────────────────┘│ 4. PutCommand / QueryCommand▼┌─────────────────────────┐│ Amazon DynamoDB ││ (On-Demand Ward Table) │└─────────────────────────┘
What each service does
- API Gateway (HTTP API v2) is the single REST entry point:
GET /wards/{wardId},POST /reports,POST /uploads/presign, andGET /health, with CORS built in.
- Lambda (
nodejs20.x) runs the stateless ingest and query logic, using the modular AWS SDK for JavaScript v3 packages (client-dynamodb,lib-dynamodb,client-s3,s3-request-presigner).
- DynamoDB (
PAY_PER_REQUEST) stores citizen reports withwardIdas the partition key andidas the sort key. It also drives the map. A report of waist-deep water at a known hotspot upgrades that hotspot fromBaselinetoVerified. If someone drops a GPS pin on a street depression I haven't catalogued, it becomes a newCrowdsourcedhotspot and triggers downhill site matching in real time.
- S3 with pre-signed uploads handles photos of flooded streets. The client strips EXIF GPS data before upload, for privacy. I also skipped base64 images through Lambda. The function issues a 5-minute pre-signed
PUTURL, and the browser uploads straight to S3.
- CloudFormation and IAM define every resource in
aws/template.yaml. The IAM roles are least-privilege, limited todynamodb:PutItem,dynamodb:Query, ands3:PutObject.
The Lambda handler
This is the handler from
aws/lambda/index.mjs :JavaScript
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";import { getSignedUrl } from "@aws-sdk/s3-request-presigner";const region = process.env.AWS_REGION || "ap-southeast-2";const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({ region }));const s3 = new S3Client({ region });const TABLE_NAME = process.env.DDB_TABLE;const BUCKET_NAME = process.env.S3_BUCKET;export const handler = async (event) => {const method = event.requestContext?.http?.method || "GET";const path = event.rawPath || "/";// 1. Ingest a geotagged citizen waterlogging report into DynamoDBif (method === "POST" && path.endsWith("/reports")) {const report = JSON.parse(event.body || "{}");const item = {wardId: report.wardId || "ward-142",id: report.id || `rep-${Date.now()}`,...report,createdAt: new Date().toISOString()};await ddb.send(new PutCommand({ TableName: TABLE_NAME, Item: item }));return { statusCode: 201, body: JSON.stringify({ synced: true, id: item.id }) };}// 2. Generate a 5-minute S3 pre-signed URL for field photosif (method === "POST" && path.endsWith("/uploads/presign")) {const { fileName, contentType } = JSON.parse(event.body || "{}");const key = `reports/${Date.now()}-${(fileName || "photo.jpg").replace(/[^a-zA-Z0-9._-]/g, "_")}`;const command = new PutObjectCommand({Bucket: BUCKET_NAME,Key: key,ContentType: contentType || "image/jpeg"});const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });const publicFileUrl = `https://${BUCKET_NAME}.s3.${region}.amazonaws.com/${key}`;return { statusCode: 200, body: JSON.stringify({ uploadUrl, publicFileUrl }) };}// 3. Fetch ward telemetry from DynamoDBif (method === "GET" && path.includes("/wards/")) {const wardId = path.split("/").pop() || "ward-142";const result = await ddb.send(new QueryCommand({TableName: TABLE_NAME,KeyConditionExpression: "wardId = :w",ExpressionAttributeValues: { ":w": wardId }}));return { statusCode: 200, body: JSON.stringify({ wardId, reports: result.Items || [] }) };}};
Deploying without the AWS CLI
I wanted the stack to deploy from any machine, including ones without the AWS CLI. So I wrote a zero-dependency Node.js deployer (
aws/deploy.mjs ) using only node:crypto . It signs requests with AWS Signature Version 4 ( AWS4-HMAC-SHA256 ), sends aws/template.yaml straight to the CloudFormation HTTPS endpoint, and polls until the stack reaches CREATE_COMPLETE . Then it writes the live API Gateway URL and S3 bucket name into .env .Checking the numbers
A civic planning tool is only useful if people trust its numbers. RechargeMatch ships with an automated verification suite (
unitTests.ts ) and a peer-review audit workspace.- No uphill recommendations. Across all 10 Ward 142 hotspots and 30 recommended site matches, the tests assert that
0sites break the gravity constraint.
- Reproducible math. The reference benchmark (
A = 20,000 m²,P = 60 mm,C = 0.85) must come out toV_runoff = 1,020 m³,V_store = 300 m³,V_drain = 96 m³, andV_gap = 924 m³.
- Back-test against engineers. I compared my top-3 site rankings for 10 South Delhi hotspots with independent blind rankings from former Delhi Jal Board and IIT Delhi hydrologists. They agreed on 28 of 30 slots (93%). The two misses were traffic islands where excavation access was a problem that 30 m DEM data can't show. I documented them rather than hiding them.
Closing the gap before the next monsoon
Waterlogging lasts a few hours, but the same depressions flood every year. Pair a physics-constrained model with an architecture that costs almost nothing when it's idle, and you get something municipal engineers, RWAs, and planners can use: a pre-monsoon priority list that turns a flood hazard into groundwater recharge.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article