More Projects
59 in total
QisPratapgarh
Containerization & deployment · Election Commission of India · 2024
Go web app for tracking voter queues at Pratapgarh polling stations. Booth admins sign in and post a live traffic count for their booth; a public page tables the latest count per station. MongoDB store, gorilla/mux with session cookies, and a public page that is an HTML shell rendered client-side from a JSON endpoint, seeded from a 566-station JSON file and shipped as a Docker image.
350K+ requests
Project Details
Containerization & deployment
Election Commission of India
2024
350K+ requests
Product · Backend
On polling day the queue is the whole experience. A voter in Pratapgarh has no way of knowing whether their booth has four people outside it or four hundred, and the only people who do know are the officers standing at the door. QIS is the smallest system that closes that loop: give every booth officer a login, let them post one number, and put all the numbers on a single public page in Hindi.
Almost every design decision here is a consequence of the day it was built for. The system has one working day, and on that day it has to survive a traffic pattern nothing else in its life will resemble: a few hundred officers writing occasionally, and an unknown number of voters reading constantly, most of them on rural mobile data with a browser that will time out before it complains. Anything clever is a liability, because there is nobody to page at 11am on polling day. So the whole thing is 364 lines of Go across four files, and the interesting parts are the ones that were deliberately left out.
What shipped
- 566 polling stations seeded from one JSON file, split across two constituencies —
Pratapgarh with 268 and Dhariawad with 298.
snoruns 1 to 566 globally whilebooth_numberrestarts per constituency, which is whysnois the session identity and the Mongo filter key everywhere. - One login per booth. A matching seed file carries 566 admin records, keyed to the booth level officer's mobile number. Every station ships with a starting count of zero and a starting timestamp of 0800, so the board is populated and coherent from the moment polls open rather than filling in booth by booth as officers get around to it.
- Session-scoped writes.
POST /admin/updatenever reads a booth id out of the request body. It pullssnooff the gorilla/sessions cookie and filters on that, so an officer cannot post a count for a booth that is not theirs even if they try. - Seven routes on gorilla/mux: the public page, a table-data endpoint, admin login, dashboard, update and logout, plus a static file server. A single session middleware wraps all of them, loading the cookie before the handler runs and saving it after.
- Two collections and four queries.
polling_stationandadmin, against one update, one find-all, one login lookup and one single-station read. There is no aggregation, no projection and no index management, because at 566 documents a collection scan is faster than thinking about it. - Timestamps as four characters.
last_updateis written ashhmmin Asia/Kolkata and split back apart client side. There is no date component, because the whole system exists for one day. The timezone was not there originally; a later commit swapped a naivetime.Now()for an explicittime.LoadLocation("Asia/Kolkata"), which matters when the container's clock is UTC and every officer reads the board five and a half hours behind reality. - Devanagari end to end, in the station names and in the public table headers.
- Everything after the first request is client side. The public page fetches all 566 rows once on load, then does pagination at ten per page, constituency filtering and substring search over the in-memory array. That is one request per visitor rather than one per interaction, and it is the reason 350K+ requests went through a small box without a cache in front of it.
- 10-second read and write timeouts on the
http.Server, which matter more than anything else when every client is on patchy rural mobile data.
The data model, such as it is
Two structs, twenty-four lines. PollingStation carries the object id, the Devanagari name, sno,
booth_number, traffic_count, cons and last_update. Admin carries a username, a password,
sno, booth_number and cons. The pairing on sno is the entire authorisation model: an admin
document names exactly one station, the login handler copies that sno into the session, and the
update handler filters on it.
Both structs are tagged for bson and nothing else, which means the table-data endpoint serialises
Go field names straight through — BoothNumber, TrafficCount, LastUpdate — and the public
page's JavaScript reads those exact keys. It is not a designed API contract so much as the absence
of one, and it works because there is exactly one producer and one consumer and they were written
in the same afternoon.
The server template for the public page is executed with an empty map. Nothing is rendered into it. The Go side's only real job on that route is to hand over an HTML shell; the table, the pagination, the constituency dropdown and the search all exist only in the browser.
What I owned
Containerisation and deployment. The image is single-stage on golang:1.21.6, with go.mod and
go.sum copied ahead of the source so dependency resolution caches in its own layer. Compose runs
it as one service on port 5001 with restart: always, pointed at a managed Atlas cluster rather
than a database container, so there is exactly one process that can die on election day and it
comes straight back.
That last point is the deployment argument in full. A database container would have doubled the
number of things that can fail and added a volume whose loss would be unrecoverable mid-poll.
Atlas moves the durable half of the system to somebody whose job is keeping it up, and leaves a
stateless Go process that restart: always can resurrect without anybody noticing.
Honest scope
This was built and shipped in days, and the shape shows. "Live" means as fresh as your last page
load: there is no polling and no server-side cache, so a voter refreshes to see a new number. The
container runs go run instead of a compiled binary in a slim second stage, which costs a
recompile on every start. Both are week-two problems, and there was no week two.
The rest of the honest list. Credentials are compared as plaintext equality inside the Mongo
filter, with no hashing and no rate limiting on the login route. The connection string, the
database name and the session signing key are literals in the source with no environment
variables anywhere, and the session store is constructed twice with the same key in two different
files. The handlers use text/template rather than html/template, so nothing rendered from the
database is contextually escaped. The find-all helper calls log.Fatal on a query error, which
means a transient Atlas hiccup takes the process down and restart: always is doing more work
than it looks like. Pagination and filtering also disagree: the page count is computed from the
unfiltered array, so paging after a search quietly reverts to the full list. The two seed files
are checked in but no seeder is, so the data was loaded out of band and the repo carries the
payload without the mechanism.
Project Details
Containerization & deployment
Election Commission of India
2024
350K+ requests
Product · Backend