Rebuilding a statewide organisation's hierarchy from an empty repo
A field organisation of 89K people ran on a vendor portal whose data could not be trusted. I audited it, threw it away, specced the replacement and directed AI coding agents to ship an API, mobile app and web console on AWS with 240K+ clean records.
- When the data can't be trusted, a cutover beats a migration. Moving bad rows across keeps the bad rows.
- Model the work the way it is actually done. Events with no due date fixed a product that was full of fake "overdue" items.
- Make the dangerous command hard to run. Production writes need a flag on every command, not an environment variable someone forgets is set.
A large statewide membership organisation in Maharashtra ran its whole hierarchy on a portal built by an earlier vendor. The portal could not reliably answer basic questions, such as who holds which post and which areas are covered. My first job was to understand that system. I ended up replacing it.
The problem#
The organisation has four separate ladders of posts, with 13, 13, 14 and 4 rungs. The old portal assumed one uniform ladder of 14, so every screen that counted people by level was wrong before anyone typed a name.
When I audited the database the problem got bigger. About 48,000 of the “members” were produced by a seeding script, a budget algorithm that builds a geographically plausible hierarchy out of invented people. Posts had no unique index, so each re-seed multiplied them. The connection credentials were committed to the repository. Nothing in the data said which rows were real.
The real question was not “how do we improve this portal” but “what would it take for a leader to trust a number on the screen?”
The decision: cut over, don’t migrate#
I proposed a new build on a clean database rather than a migration. A migration would have carried 48K invented people into the new system, and nobody would ever have been able to separate them out again. So the plan was:
- Start from an empty repository, using the old code only as a reference for the domain.
- Load only data with a named source: the organisation’s own rosters and public geography registries.
- Delete the old data as test data and stop running the old app.
The client agreed, and the cutover happened in one day.
What I built#
- One API (Express on AWS Lambda behind API Gateway, infrastructure in OpenTofu) serving an Expo app for Android and iOS, a web console and a desktop wrapper.
- The hierarchy as data. Each of the four ladders keeps its own length. A hard-coded ladder length counts as a bug, and a single shared module answers “how many levels are below this post”.
- Geography from registries. 151,389 areas, down to 44,198 villages and 100,186 local units, every row traceable to a named source. 89,218 members sit on top of it, and 99.8% of them are placed at exactly the right level of area. Where a member’s area could not be matched to their post, the system stores no area and a reason. It does not keep a wrong one.
- Events instead of tasks. The first version had tasks with due dates. In the field, one instruction sent to 40 local leads becomes 40 gatherings on 40 different days, so a single due date made almost everything “overdue”. I replaced tasks with events: a lead sends one down their branch saying what it is and why, and the person who holds it records when, where and how many came, then posts proof. Nothing in the product is overdue.
- An AI assistant scoped to each user’s branch, so it can only see the part of the organisation the user can see. It takes voice in and out. Marathi readout runs on Google Cloud TTS because Amazon Polly has no Marathi voice.
Guardrails that mattered more than features#
- Production writes are deliberate. Any command that touches the live database needs a
--remoteflag on that command, and the database must have one of the product’s own names. The old escape hatch was an environment variable. Once it was set in a shell profile, it quietly allowed every later command, including deletes. - Sensitive fields are not shown just because they exist. The client’s roster included age and caste for senior leaders. No feature uses caste, and I flagged that no lawful basis was recorded for it.
- Verify by string as well as by hash. A hash proves that a deploy landed. A string in the served bundle proves which one did.
- 1,582 automated tests, including a frozen payload contract. The defect I saw most often was a value that existed in the database and never reached the screen, because a projection or filter left it out. Thirteen of those were caught, most of them by opening a screen while the test suite was green.
Results#
- 240K+ records live on the new cluster (89,218 members, 151,389 areas), with zero orphan, ancestor or area breaks.
- Every route is authenticated. The app is live on AWS and serves the real roster.
- I wrote down open questions as questions for the client instead of guessing. For example: which assembly seat covers which sub-district, and 48 posts that hold people but were never officially appointed. The system shows these gaps. It does not paper over them.
What I’d do differently#
I would ask for the client’s own rulebook for the hierarchy on day one, before reading the old system at all. The old portal’s uniform 14-rung ladder looked authoritative because it was in code, and it was wrong. On a data-heavy zero-to-one build, the document that defines the domain is the first deliverable, before any screen.
- Node/Express on AWS Lambda
- API Gateway
- MongoDB Atlas
- Expo app for Android and iOS
- React web console
- OpenTofu
- OpenAI
- Google Cloud TTS