License plate reading with Bun: the lesson from the watched YouTuber
A YouTuber built a camera that reads car license plates, in the style of the ones Flock Safety is spreading across the United States. But he pointed it at the police. Shortly after, he says, a few officers showed up at his door. The story ran on Gizmodo and got me thinking about something very practical: today, building license plate reading with Bun, a cheap camera and an OCR model is a weekend project. And that's exactly the problem.
What happened, in a few lines
Flock sells automatic license plate reader cameras (ALPR) to cities, police departments and even gated communities. The camera photographs every passing car, extracts the plate and sends everything to a searchable database. Thousands of cities already use it, and groups like the ACLU have criticized the model for years: lots of people with access, unclear retention rules and almost no transparency.
The YouTuber decided to flip the script. He built his own setup, recognized police cruisers and logged where they went. In other words, he did to the police exactly what the police do to everyone else. The reaction came fast.
I won't get into the legal side of the case. That depends on the state and on details the story doesn't settle. What interests me is something else: the tool is the same on both sides. The only thing that changes is who's in front of the lens.
When the watched build the same camera, everyone suddenly discovers privacy exists.
Why this matters for people who write code
Because the hard part of this system stopped being hard. Ten years ago, ALPR required dedicated hardware and expensive software. Today you have:
- an IP camera or a Raspberry Pi with a camera module;
- an open source plate detection model;
- a backend that receives the readings and writes them to a database.
That last part is what any full stack dev does every week. One endpoint, one insert, one lookup by plate. I'd write it in Bun in half an hour, installing almost nothing, because the runtime ships with an HTTP server, SQLite and crypto out of the box.
And that's where the lesson lives. Surveillance code looks exactly like the code for a parking app, a front desk access system, a fleet tracker. The difference is in the decisions you make about the data.
License plate reading with Bun, the responsible way
Here's the backend I'd build for a legitimate case: a parking lot that needs to know whether a car that came in has left. No tracking people. The point is to see where the choices are that separate a useful system from a surveillance machine.
import { Database } from "bun:sqlite";
import { createHmac } from "node:crypto";
const db = new Database("readings.db");
db.run(`CREATE TABLE IF NOT EXISTS readings (
plate_hash TEXT NOT NULL,
gate TEXT NOT NULL,
read_at INTEGER NOT NULL
)`);
const SECRET = process.env.PLATE_SECRET!;
const RETENTION_MS = 72 * 60 * 60 * 1000; // 72 hours and that's it
const hash = (plate: string) =>
createHmac("sha256", SECRET).update(plate.toUpperCase()).digest("hex");
Bun.serve({
routes: {
"/reading": {
POST: async (req) => {
const { plate, gate } = await req.json();
db.run("INSERT INTO readings VALUES (?, ?, ?)", [hash(plate), gate, Date.now()]);
return new Response(null, { status: 204 });
},
},
},
});
setInterval(() => {
db.run("DELETE FROM readings WHERE read_at < ?", [Date.now() - RETENTION_MS]);
}, 60 * 60 * 1000);
There are three decisions in this snippet, and none of them is really technical:
- The plate isn't stored in plain text. It goes in as an HMAC with a secret. You can check "did this car come in and leave?", but nobody who leaks the database can list plates.
- The data has an expiration date. 72 hours and it's gone. A parking system doesn't need to remember where your car was in March.
- There's no search endpoint. If nobody asked for "history of where plate X went", that route doesn't exist. What doesn't exist can't leak and can't be subpoenaed.
Notice the code gets smaller with these choices. Privacy here isn't an extra feature. It's cutting scope.
"But hashing the plate doesn't help, you can brute force it"
It's a good objection, and it's partly right. Plates have a known format. In Brazil, the Mercosur standard has somewhere around 450 million combinations. With plain SHA-256, a home GPU tests all of them in minutes.
That's why the example uses HMAC with a secret, not a plain hash. Without the key, brute force gets you nowhere. With the key, the attacker already has access to the server and you have bigger problems. If you want to go further, keep the secret off the machine (in a secrets manager) and rotate the key along with the retention cycle. Old readings become impossible to correlate.
It's not perfect. But it's much better than the industry default, which is usually plain text plates, a photo of the car and 30 days of retention or more, "just in case".
What the YouTuber's case lays bare
The part of the story that got me most wasn't the police visit. It was the discomfort. When someone builds the same system and aims it at the people who usually run this kind of tool, the feeling of being invaded shows up instantly. The technology is the same. The unease is the same. But on one side it becomes news, and on the other it becomes a public contract.
For us devs, this leaves an uncomfortable question. It's also a useful one for reviewing any system that stores location data:
- Would I be fine if this database held my movement history?
- Who can run a
SELECThere without asking anyone? - If it leaks tomorrow, what can someone learn about a specific person?
If the answer to the first one is "no", you're building a miniature Flock. And you don't need to be on a camera project to fall into this. Food delivery apps, package delivery apps, fleet trackers, gym check-ins: they all generate a location trail. And almost all of them keep more than they need.
What changes in your workflow
In practice, I take three habits from this case into any project:
- Retention goes into the schema, not the backlog. Date column and cleanup job in the same PR that creates the table. If it's left for later, it never happens.
- Sensitive data becomes an opaque identifier as early as possible. HMAC at the door, not in an "anonymization" script run once a quarter.
- Every search route needs a written reason. If the reason is "it might be useful someday", it doesn't ship.
Bun helps here in a slightly boring way. Since SQLite, the server and crypto already come with the runtime, there's less room for the "I'll set it up later" excuse. The example above has zero dependencies to install. You can do it right from the first commit.
My take
I think the YouTuber accidentally gave the best possible demonstration of what people have been saying about ALPR: the problem was never the camera, it was the database behind it. And we are the ones who design that database. The police, Flock or the YouTube guy just choose where to point it.
So next time you create a table with a plate, an ID number or a coordinate, think about this case for ten seconds. Expiration date, opaque identifier, no search without a reason. It costs about twenty lines. If you want to see how I apply this in real projects, take a look at my projects.
LinkedIn summary
A YouTuber pointed a license plate reader at the police. Shortly after, he says, the police knocked on his door. The tool is the same on both sides. The only thing that changes is who's in front of the lens. Today, building plate reading is a weekend project. The backend is one endpoint, one insert and one lookup. In Bun I can do it in half an hour. That's why the problem was never the camera. It's the database behind it, and we are the ones who design that database. Three habits I bring to any project with plates, ID numbers or location: retention goes into the schema, sensitive data becomes an HMAC right at the door, and no search route ships without a written reason. Privacy here isn't an extra feature. It's cutting scope. I wrote the full article with the code. If this topic matters to you, it's worth the read. #Privacy #SoftwareDevelopment #Bun #GDPR #InfoSec