AWS Builder Center
The Leak Nobody Pays For: Building AquaLedger, a Serverless Water Ledger for Apartment Societies

The Leak Nobody Pays For: Building AquaLedger, a Serverless Water Ledger for Apartment Societies

How I built AquaLedger, a serverless water ledger for apartment societies on AWS. It catches leaks overnight, bills each flat for the water it actually used, and plans tankers, all inside the free tier.

It's 3 a.m. in a 40-flat apartment society in Pune. Everyone is asleep, but in flat B-202 a cistern valve hasn't seated properly, and water is trickling into the bowl at 20 litres an hour. Nobody hears it. Nobody will notice for weeks.
By May the sump runs dry, and the committee calls a tanker: 10,000 litres for ₹1,800–2,200. Then another, and another. At the end of the month the bill is split equally across all 40 flats. The retired couple in A-101, who use barely 200 litres a day, pay exactly the same as the family of six upstairs, and the same as B-202, whose leak just cost everyone about ₹2,700.
I built AquaLedger so that this story ends differently.



The problem: three water sources, one spreadsheet, zero visibility

Indian housing societies buy water from three places:
Source
Typical cost
The catch
Municipal supply
~₹22 per kL
Rationed: it never covers summer demand
Borewell
~₹9 per kL
Yield drops sharply in summer
Private tankers
~₹180+ per kL
Fills the gap, at about 8× the municipal price
That last row changes how you should think about a leak. Every litre a leak wastes is effectively bought at the tanker price, because tankers are what fill the gap.
Most committees run all this from a notebook and a WhatsApp group, which leads to three recurring problems:
  1. Leaks hide for weeks. A running cistern wastes about 480 L a day, and nobody finds out until the bill arrives.
  1. Equal-split bills cause disputes. Small households subsidise heavy users and leaking flats. It's the most common argument at society meetings.
  1. Tankers are booked in a panic, because nobody knows how much water next month needs.

What AquaLedger does

AquaLedger is a web app for the society committee, plus a simple portal for residents.

  • Catches leaks while everyone sleeps. A nightly scan checks every flat for unusual daily usage and for water running between 2 and 4 a.m. Smart meters can raise an alert within seconds.
  • Bills every flat fairly. Each flat pays its share of metered litres plus a fixed common-area charge, in one click, next to what it would have paid under the old equal split. In the sample society, 19 of 40 flats pay less.
  • Plans tankers ahead. A seasonal forecast gives next month's demand, the number of tankers to book and the budget.
  • Answers plain-English questions such as "Why did our bill go up this month?", worked out from the society's own numbers.
  • Gives residents a window, not a login. Every flat gets an access code by WhatsApp, email or a printed slip. Residents see only their own flat.

The architecture

I had two non-negotiables: it must cost almost nothing when idle, because a 40-flat society won't pay for servers running at 4 a.m., and one society must never see another's data. That pointed straight at a fully serverless design on AWS, defined end to end with AWS Amplify Gen 2.

Layer
AWS service
Role
Hosting
AWS Amplify Hosting
CI/CD on every push; serves the static Next.js export from a CDN
Identity
Amazon Cognito
Committee sign-up with email verification; one group per society; guest identity for residents
API
AWS AppSync
GraphQL with group-based authorization and custom Lambda resolvers
Data
Amazon DynamoDB
8 tables, pay-per-request, with per-society secondary indexes
Compute
AWS Lambda (ARM, Node 22)
admin-ops, query-api, analysis and meter-ingest
Meter endpoint
Lambda Function URL
Public HTTPS endpoint for smart meters
Scheduling
Amazon EventBridge Scheduler
Nightly leak scan at 01:15 IST
Alerts
Amazon SNS
One topic; a filter policy routes each alert to its own committee
Logs
Amazon CloudWatch Logs
JSON logs with one-month retention
Optional
Amazon SES, AWS End User Messaging Social, AWS Budgets
Resident logins by email or WhatsApp; a spending alarm
The rest of this post walks through the parts I found most interesting.

1. Multi-tenancy in one line: Cognito groups as tenants

Each society is a Cognito group whose name is the society's ID, and every record carries a  societyId . Amplify Gen 2's  groupDefinedIn  rule then does the hard part. AppSync only lets a user read or write a record if they belong to the group named in that record's  societyId  field.
Plain text
Society: a .model({ societyId: a.id().required(), name: a.string().required(), deviceToken: a.string(), adminEmails: a.string().array(), lastNightlyScanAt: a.datetime(), // ... }) .identifier(["societyId"]) .authorization((allow) => [allow.groupDefinedIn("societyId")]),
When a committee member creates a society, the  admin-ops  Lambda creates the Cognito group and adds them to it. Inviting a treasurer means adding them to the same group. Isolation is enforced by AppSync on every request, not by  if  statements I might forget.

2. Catching leaks while everyone sleeps

The leak logic lives in plain TypeScript ( lib/core/leak.ts ) with no AWS dependencies. The Lambdas and the UI share it, and unit tests cover it. Two rules run every night.
Spike rule. It compares yesterday's usage with the flat's normal, defined as a 30-day mean that ends a week earlier. The one-week gap matters: without it, a leak that started five days ago slowly becomes part of its own baseline and stops being flagged.
Plain text
const m = mean(baseline);const sd = Math.max(stdDev(baseline), m * 0.05);const score = (today - m) / sd;if (score > 2.5 && today > m * 1.3) { // flags e.g. "B-202 used 694 L on 9 Oct, 3.4× its normal 203 L a day."}
Night-flow rule. It flags steady flow in every hour between 2 and 4 a.m. on 3 or more of the last 7 nights. Real households flush occasionally at night; they don't run water continuously. A cistern does. In the sample society it produces: "B-202 shows steady flow of ~20 L/h between 2 and 4 a.m. on 5 of the last 7 nights: likely a running tap or cistern leak (~481 L/day wasted)."
Scheduling it took one line in the function definition, and EventBridge Scheduler does the rest:
Plain text
export const analysis = defineFunction({ name: "analysis", entry: "./handler.ts", timeoutSeconds: 120, memoryMB: 512, schedule: "45 19 * * ? *", // 19:45 UTC = 01:15 IST, every night ...LAMBDA_DEFAULTS, // arm64, Node 22, JSON logs, 1-month retention});
On the first of each month, the same job also bills the previous month and writes a monthly summary for the committee.

3. Live smart-meter alerts with a Lambda Function URL

For smart meters I didn't want to set up API Gateway, certificates or IoT policies for a hackathon. A Lambda Function URL gives a public HTTPS endpoint in a few lines of CDK. The function is defensive because it's public:
Plain text
// public endpoint: reject anything that can't be a meter reading before touching the databaseif ((event.body?.length ?? 0) > 4096) return json(413, { error: "Request too large" });// no household tap or burst pipe delivers more than ~200 L/min through a domestic meterif (Number(litres) / periodMinutes > 200) return json(400, { error: "Implausible flow rate" });
const society = await getSociety(String(societyId));const token = event.headers["x-device-token"] ?? "";if (!society?.deviceToken || !safeEqual(token, society.deviceToken)) return json(401, { error: "Bad device token" });
A valid reading is stored, compared with the flat's 7-day average flow rate, and, if it is far above normal, it raises a  live_spike  alert. That's de-duplicated to one per flat per 30 minutes, and it emails the committee. In the demo, you open a "tap" on a test meter and the alert appears within seconds.
The committee can test the whole chain from the Live meters page, which sends readings from the browser through the same endpoint. That page also shows the endpoint URL, the society ID and a masked device token that can be rotated from Settings.

4. Alerts that reach only the right committee: SNS filter policies

Instead of a topic per society, there is one SNS topic. Each committee member's email subscription carries a filter policy on  societyId :
Plain text
await sns.send( new SubscribeCommand({ TopicArn: topicArn(), Protocol: "email", Endpoint: email, Attributes: { FilterPolicy: JSON.stringify({ societyId: [societyId] }) }, }));
Publishing a leak alert with the  societyId  message attribute delivers it only to that society's committee. That means no per-tenant infrastructure, and SNS email is in the always-free tier.

5. Fair bills, explained

Billing is deliberately simple, so a resident can check it by hand:
flat's bill = (flat's litres ÷ all metered litres) × society's total water cost + common-area charge
Each bill shows the flat's share, the usage charge, the common charge and what the flat would have paid under the equal split. Generating bills for all 40 flats is one click. Re-running never touches bills already issued or marked paid.

6. Residents without accounts

Asking 40 households to create accounts and remember passwords is a non-starter. Instead, the committee clicks Give access to all flats. Every flat gets a random access code, sent by WhatsApp (one tap from the committee's own phone, free) or email, or printed as slips for letterboxes.
Only a hash of the code is stored:
Plain text
export async function hashAccessCode(input: string): Promise<string> { const data = new TextEncoder().encode(`aqualedger-flat-access:${normalizeAccessCode(input)}`); const digest = await globalThis.crypto.subtle.digest("SHA-256", data); return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");}
The resident portal calls a  residentPortal(code)  query through a Cognito guest identity. The Lambda finds the flat by hash and returns only that flat's view. Issuing a new code switches off the old one immediately.

7. A System check that explains itself

Serverless apps have many moving parts that can be silently misconfigured: an unconfirmed SNS subscription, a schedule that never ran, a meter endpoint with the wrong permissions. Settings → System check tests each one live and says how to fix it. For example:
  • Alert emails: "Waiting for confirmation: treasurer@… → open the 'AWS Notification - Subscription Confirmation' email."
  • Nightly leak scan: the job writes  lastNightlyScanAt  on each run, which proves the EventBridge schedule actually fires.
  • Smart-meter endpoint: the check POSTs a reading with a deliberately wrong token and expects a 401.

What it costs

At society scale, the whole stack sits inside the AWS free tier: Lambda's 1M requests a month, DynamoDB on-demand, SNS email, EventBridge Scheduler and Amplify Hosting. Nothing is always on. Lambdas run on ARM/Graviton (about 20% cheaper per GB-second), logs expire after a month, and an optional AWS Budgets alarm emails you at 80% and 100% of a limit you set.
That's what makes the business model work. AquaLedger is priced at ₹10–15 per flat per month, so a 40-flat society pays about ₹480 a month. One caught leak like B-202's (481 L a day, about ₹2,700 a month at tanker prices) pays for more than five months.

What went wrong (and what I learned)

  • Amplify treated my static site as server-rendered. The app is a static Next.js export, but it was created as a compute (SSR) app, so every page returned 404. The fix was  aws amplify update-app --platform WEB . Check the platform first if a static Amplify build "succeeds" but serves nothing.
  •  npm ci  failed in the Amplify build image with lockfile errors I couldn't reproduce locally. They were caused by bundled dependencies. Switching the build to  npm install --prefer-offline  fixed it.
  • Table permissions didn't cover the indexes. Amplify exposes model tables as imported tables, so  grantReadWriteData  doesn't include secondary indexes. I added  dynamodb:Query  on  table/index/*  explicitly.
  • Reserved concurrency broke deploys on a new account. New accounts have a 10-execution limit, and AWS rejects any reservation that leaves fewer than 10 unreserved. I removed it and validate input before any database write instead.
  • I dropped generative AI and the product got better. I originally planned to have Amazon Bedrock write the monthly summaries and chat answers, but model access wasn't available on my account. I replaced it with rule-based summaries and keyword answers computed from the dashboard numbers. They're free, instant and never fail.
  • Weekends caused false leak alarms. Testing on 11 October, the sample data flagged two healthy flats: people use about 10% more water on weekends, and the September-to-October seasonal step added a little more. Requiring a spike to be 30% above normal (as well as 2.5 standard deviations) removed every false alarm across six weeks of dates, and the real leak is still caught.

What's next

  • Pilot with two or three real societies in Pune.
  • Integrations with smart-meter vendors and building-management gateways.
  • Automatic WhatsApp and email logins at scale (WhatsApp Business through AWS End User Messaging Social, and SES production access).
  • Online bill payment, so the ledger closes the loop.

Try it

  • Live app:  https://main.d200aasztxvlg1.amplifyapp.com . Choose Committee login → Create Account, then tick Start with 6 months of sample data when you set up the society. Then go to Leak alerts → Scan for leaks now and watch flat B-202 get caught.
Water is the one utility where a society pays the highest price for its own waste. AquaLedger makes that waste visible the morning after it starts, not at the end of the month.
Built by Utkarsh Sonthalia with AWS Amplify Gen 2, Amazon Cognito, AWS AppSync, Amazon DynamoDB, AWS Lambda, Amazon EventBridge Scheduler and Amazon SNS.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article