QisPratapgarh
Completed
Product · Backend

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.

SCALE

350K+ requests

Built with
GoGo
MongoDBMongoDB
DockerDocker
Project Details

STATUS
Completed
ROLE

Containerization & deployment

ORGANISATION

Election Commission of India

YEAR

2024

SCALE

350K+ requests

TYPE

Product · Backend

TAGS
Government
Real-time
Civic Tech

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. sno runs 1 to 566 globally while booth_number restarts per constituency, which is why sno is 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/update never reads a booth id out of the request body. It pulls sno off 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_station and admin, 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_update is written as hhmm in 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 naive time.Now() for an explicit time.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

STATUS
Completed
ROLE

Containerization & deployment

ORGANISATION

Election Commission of India

YEAR

2024

SCALE

350K+ requests

TYPE

Product · Backend

TAGS
Government
Real-time
Civic Tech