BuildingTraceSarkar · 9 min read
Safe by construction: privacy engineering for a civic app on a zero budget
Faces destroyed before any model sees them, locations snapped instead of jittered, and database constraints that make the unsafe path impossible rather than unlikely.
A photograph of a pothole is rarely just a pothole. There’s a scooter rider in it, a number plate, a shopfront, and — implied by the GPS — the place where the person taking it lives or works. TraceSarkar publishes evidence. So privacy couldn’t be a feature; it had to be a property of the pipeline.
Our rule of thumb: don’t write code that checks whether something is safe. Write code where the unsafe version can’t be expressed.
Redact before anyone else looks#
Order matters. Redaction runs before classification, so no face ever reaches a third-party model. That ruled out cloud vision APIs for detection, so both detectors run in our own binary:
- Faces — the Pico cascade via the MIT-licensed
pigolibrary, embedded at build time. - Number plates — a small, homegrown multi-scale edge-density localiser.
Detected regions are mosaicked — destroyed — not blurred, because blur can sometimes be reversed. Each box is padded about an eighth past its edge, because detectors are tight and faces are not. And an image is held unless both detectors can account for it.
A record, not a flag#
The easy design is a boolean: redacted = true. The trouble with flags is that someone can set one. Instead, every public image must point to a redaction record, and a database CHECK constraint refuses a “published” verdict that has no output object. There is no code path that produces a public image without a redaction behind it.
Snap, never jitter#
Public maps need to show roughly where a problem is without revealing exactly where the reporter stood. The standard trick is to add random noise to the coordinates. We don’t:
“Publish the same point enough times with a fresh offset each time and the mean converges on the truth.”
Public positions are snapped to a fixed 0.001° grid — about 111 metres. Snapping is idempotent: a hundred reports from one doorstep reveal exactly as much as one. And the public history type has no actor field at all, so there’s nothing to accidentally serialise.
Measure the thing you’re afraid of#
Redaction quality is usually reported as IoU or recall percentages. We measure destroyed pixels, and we print a dash rather than 100% when the set is empty:
““98% recall” reads as a pass; “one face published” reads as what it is.”
Which brings us to the honest part. Our labelled set of real captures doesn’t contain a face or a plate yet, so redaction recall is unmeasured. We’ve said so on the methodology page, the project owner has explicitly accepted that risk for the pilot, and the set grows with every real capture.
The mistakes we made on the way#
Privacy-by-construction is a goal, not a guarantee. A few things we got wrong and fixed:
- 1
2,387 phone numbers
BMC’s roads API returned representatives’ personal mobile numbers. Our blocklist named the wrong fields, so they were stored. We scrubbed them in a migration, and a test now asserts the blocklist names fields the source actually returns. - 2
A briefly public bucket
The archive bucket was public for a short window and served a capture without authentication. It’s private and locked now, and downloads live in a separate bucket. - 3
One account for everyone
For a while every capture was filed under one shared account. The middleware verified the session; the handler ignored it. - 4
Blind to big plates
The plate detector missed large, close-up plates. One annotated photograph was enough to find it.
Deletable, but not editable#
An issue’s history is hash-chained with SHA-256, so it can’t be quietly rewritten. But India’s data protection law gives people a right to erasure. So the chain is designed to allow deletion of an issue while making any edit detectable. The attack a hash chain defeats is the invisible one — and an erasure is visible by design.
Keep reading
- Bands, not percentages: a spatial join you can’t afford to get wrongHow TraceSarkar matches a photo of a pothole to the ward that owns the road and the contract that paid for it — and why every gate is built to ask rather than guess.Building · TraceSarkar
- No model in the path: why we hand-write the post and the PDFTraceSarkar uses AI to read a photograph. It refuses to use AI to write a sentence about anyone. Here’s where we drew that line, and why.Principles · TraceSarkar
- Making a housing society’s books balance — in paiseWhy ViharSync keeps a real double-entry ledger, stores every rupee as an integer, and lets Postgres refuse any journal entry that doesn’t add up.Building · ViharSync