Back to the blog
BunPrivacySecurityBackend

ICE Surveillance: The Data Lesson for Bun Developers

October 29, 2026·6 min read·Diego Horvatti

Wired published a report saying that ICE, the US immigration agency, is running one of the largest surveillance operations against left-wing movements in the country's history. This ICE surveillance case sounds like a politics story, not a code story. But every operation this size runs on software. Someone wrote the endpoint that stores the IP. Someone decided the log would live forever. And that someone most likely had no idea what it would be used for later.

The full story is here: ICE Is Running One of the Largest Anti-Left Surveillance Operations in US History. It's worth reading. Here I want to talk about our part, as people who write backend code every day.

What the report shows

I won't retell the whole story, and you should read the source before drawing conclusions. The core point is simple: a government agency gathered information about people and groups because of their political views. That information comes from somewhere. Social media, commercial databases, public records, data matching.

None of those databases were built to watch activists. They were built to sell ads, prevent fraud, measure engagement. They became surveillance tools because they were there, full, ready to be queried.

This is the pattern behind almost every data scandal. The villain is rarely a system designed for harm. It's an ordinary system that kept too much, for too long.

Why this matters if you write code

Because data you don't collect can't be requested by anyone. Not by a government, not by an attacker, not by a curious intern with access to the production database.

Think about the last project you shipped. It probably has:

  • Request logs with IP, user agent and request body
  • A users table with phone, address and date of birth "just in case"
  • Analytics storing per-user events since day one
  • Database backups with no expiration date

None of this is malice. It's misdirected laziness. And I say that as someone who has done all of it.

Data kept for no reason is a liability waiting for an owner.

The part few people face: the decision to retain is technical, but the consequence is political. You don't control who will ask for this data five years from now. You only control what exists to be asked for.

How to reduce collection in a Bun backend

Now the practical part. I use Bun in most of my API projects, so the examples use it. The logic works for Node, Deno or anything else.

The first step is the most boring one: look at what you log. A naive request logger usually looks like this:

Bun.serve({
  async fetch(req, server) {
    const body = await req.clone().text();
    console.log({
      ip: server.requestIP(req)?.address,
      ua: req.headers.get("user-agent"),
      url: req.url,
      body,
    });
    return handle(req);
  },
});

This stores the IP, the browser, the URL with its query string (which sometimes carries an email or a token) and the entire body. If someone sends a password in a POST, it ends up in the log. I've seen this in production more than once.

A more honest version logs only what helps with debugging:

Bun.serve({
  async fetch(req) {
    const { pathname } = new URL(req.url);
    const start = performance.now();
    const res = await handle(req);
    console.log({
      method: req.method,
      path: pathname,
      status: res.status,
      ms: Math.round(performance.now() - start),
    });
    return res;
  },
});

No IP, no body, no query string. In practice, I've almost never missed them. When a bug needs more context, I turn on a temporary detailed log and turn it off afterwards.

What if I need to identify the user?

Sometimes you really do. Rate limiting by IP is the classic example. The answer is to store something you can compare, but can't use to identify.

Bun has native hashing, with zero dependencies:

const salt = Bun.env.IP_SALT!; // rotate periodically

function ipKey(ip: string) {
  return new Bun.CryptoHasher("sha256").update(salt + ip).digest("hex");
}

With a salt that changes, say, every day, you can rate limit requests without building a history of who accessed what. When the salt rotates, the old hash becomes useless junk. An honest warning: the IPv4 space is small, so a hash with a fixed, leaked salt can be brute-forced. The rotation is what does the real work.

For passwords, don't even think about it. Use Bun.password.hash, which uses argon2 by default. If you're still storing passwords with plain SHA in 2026, this article is for you twice over.

How long should I keep data?

The short answer: the minimum the feature requires, with the deadline written in code. Brazil's data protection law (LGPD) already requires this through its necessity principle, and GDPR has the same idea. Most projects ignore it because nobody enforces it.

I like to make retention a database rule, not a promise in the README. With bun:sqlite it looks like this:

import { Database } from "bun:sqlite";

const db = new Database("app.db");

// analytics events live for 30 days
db.run(`DELETE FROM events WHERE created_at < datetime('now', '-30 days')`);

In PostgreSQL it's the same idea, running as a scheduled job. What matters is that cleanup is automatic. Retention that depends on someone remembering isn't retention. It's intention.

Some rules I've adopted and recommend:

  • Application logs: 7 to 14 days
  • Per-user analytics events: aggregate them and delete the raw data after 30 days
  • Deleted accounts: actually delete them, not just set deleted_at
  • Backups: with an expiration date, and tested (an eternal backup is just a parallel database with no access control)

Soft delete deserves a comment. It's great for an "undo" button. It's terrible as a privacy policy. If a user asked to leave, the record with deleted_at filled in still answers any query that ignores that column. Including a query made under a court order.

The usual objection: "what if I need it later?"

I hear this in every product meeting. And I get it. Data feels free, and throwing it away feels wasteful.

But do the full math. Every stored field has a cost in storage, backups, compliance and incident response. And it has a cost that doesn't show up on the AWS bill: the risk of it being used against the person who trusted your system.

In my experience, the "just in case" almost never comes. When it does, you start collecting from that point on and that's it. Losing three months of history rarely breaks a product. Leaking three years of history can break the company.

And yes, I know my to-do list app isn't a target for the US government. Yours probably isn't either. But the databases that show up in reports like this didn't think they were targets either. They were just startups with a generous SELECT *.

My take

I don't think developers are to blame for every abuse of data. The bigger responsibility lies with whoever decides to use it. But I think we underestimate the power we have when we write the schema. One less column, a shorter log, a cleanup job running every night. These are small decisions that fit in a 20-line PR, and they change what exists to be watched.

Bun makes this easier because it ships hashing, passwords and SQLite with no extra packages. There's no more "it's a pain to set up" excuse. What's missing is the habit of asking, before every new field, whether it really needs to exist.

If you want to see how I apply this in my own systems, take a look at the projects I maintain.

LinkedIn summary

Data you don't collect can't be requested by anyone. Not a government, not an attacker, not a curious intern.

Wired reported that ICE built one of the largest political surveillance operations in US history. None of the databases it used were built to watch anyone. They were built to sell ads and measure engagement.

The villain is almost never a system built for harm. It's an ordinary system that kept too much, for too long.

Logs with IPs and request bodies. Phone numbers "just in case". Backups with no expiration. I've done all of it.

In Bun the fix is small: logs without personal data, IP hashing with a rotating salt, Bun.password for passwords and a scheduled DELETE for retention. A 20-line PR changes what exists to be watched.

I wrote the step by step with code on the blog. Before you add the next field, ask whether it needs to exist.

#Privacy #DataProtection #Bun #Backend #SoftwareDevelopment