← Back to Blog
EngineeringAugust 23, 202625 min read

What It Actually Takes to Build a Privacy Layer Between People and AI

Engineering notes from building ARKN — the architecture, the failures, the trade-offs, and why we're only getting started.

When we started building ARKN, the idea sounded deceptively simple:

Protect sensitive information before it reaches AI.

A user types something into ChatGPT, Claude, or Gemini. ARKN detects anything sensitive, replaces it with a token, sends the protected version to the AI, and restores the original information locally when the response comes back.

The important part was the word before.

We didn't want to build another service that receives a user's private prompt, analyzes it on a server, and then tells them whether it was safe. That would mean the privacy layer itself had to see the information we were trying to protect.

So we made a strict architectural decision from the beginning:

Raw prompts and unredacted sensitive information should never leave the user's device. Not even to ARKN.

That decision sounds simple. Building around it wasn't.

1. Starting With Regex

The first version of ARKN's detection engine was entirely regex-based. No machine learning model. No external inference API. No LLM deciding whether something looked sensitive. And that was intentional.

A large part of the sensitive information we wanted to protect is highly structured. Email addresses, phone numbers, National Insurance numbers, NHS numbers, bank details, postcodes, driving licence numbers, and court claim references all have recognizable, auditable patterns.

For those types of information, deterministic detection has some very attractive properties: it is fast, requires almost no compute, adds nothing to the extension's footprint, and is completely explainable. For a compliance-conscious product, there is something reassuring about being able to say:

“This value was detected because it matched this rule.”

Rather than: “A model thought this looked sensitive.”

For names and organizations, we layered contextual heuristics on top — rules for honorifics like “Dr James”, greetings like “Dear Femi”, sign-offs, social contexts, a 300+ name dictionary covering Yoruba, Hausa, Igbo, South Asian, Arabic, and European naming systems, and legal organization suffixes like Ltd, LLP, PLC.

It worked — for the cases we had tested. And that distinction would become important.

2. The Browser Became the Hard Part

ARKN is a Chrome extension built around Manifest V3. That architecture comes with an important restriction: content scripts run in an isolated JavaScript world and cannot directly modify the page's fetch or XMLHttpRequest implementations.

But network interception was fundamental to what we were trying to do. That forced us to introduce a MAIN-world component that could patch the page's network APIs.

Browser Page
     │
     ▼
MAIN World (fetch / XHR interception)
     │
     ▼
Isolated Content Script
     │
     ▼
Service Worker
     ├── Detection
     ├── NER
     ├── Policies
     └── Telemetry

Data crosses those boundaries through postMessageand DOM custom events rather than shared state. This is one of those architectural decisions that users will never know exists. And that's the point — if ARKN is doing its job, someone should be able to open ChatGPT, type normally, and never think about the machinery underneath the page.

3. The Detection Pipeline

At this point we stopped thinking about ARKN as a collection of regexes and started thinking about it as a detection engine. The pipeline became layered:

Prompt
  ├── Regex
  ├── Dictionary
  ├── Context
  └── NER
        │
        ▼
Merge candidates
        │
        ▼
Score candidates
        │
        ▼
Apply policy threshold
        │
        ▼
Protected prompt

Every detector returns a candidate with a start offset, end offset, the matched text, entity type, confidence score, and the detector ID that produced it. The engine merges overlapping candidates, resolves conflicts by longest span then highest confidence, scores with contextual boosts, and replaces final entities with tokens like {NAME_1}. The originals are kept in a local session map — never uploaded.

That layered architecture turned out to be one of the most important decisions we made. Because eventually, regex wasn't enough.

4. The Test That Broke Everything

The moment wasn't theoretical. It was a normal test prompt:

“Draft an email to Femi Balogun at Ascendia Tech regarding his salary. His number is 081-38-55-8-7-1-4-5 and he's based in Lagos, Nigeria.”

A human reads that sentence and immediately sees several pieces of sensitive information. ARKN didn't. We found five distinct failure modes in one prompt:

  • Femi was in our dictionary. Balogun wasn't. Yoruba surnames weren't covered. The name was split, only the first token caught.
  • Ascendia Tech was completely invisible — our org detector required a legal suffix like Ltd or Inc.
  • 081-38-55-8-7-1-4-5 — Nigerian mobile format. Our phone detector matched UK patterns only.
  • Lagos, Nigeria — no location detector existed at all.
  • Lowercase femi — our dictionary assumed a capitalized first name.

The problem wasn't that our dictionary wasn't big enough. It was the assumption that names and organizations could be represented by a finite list. We needed language understanding.

5. Putting NER in the Browser

The obvious next step was Named Entity Recognition. But we couldn't send the prompt to an external NER API — that would violate the fundamental architecture. The model needed to run locally.

So we introduced a lightweight NER model using Transformers.js and ONNX WebAssembly, running inside the extension's service worker. That changed the engineering problem considerably. Our extension had previously been tiny. Now we were shipping a machine-learning model — roughly 40 MB for the current model and its data.

We also introduced inference latency. Regex executes in microseconds. A neural model has to be initialized, loaded, and run. First inference on a cold start can take substantially longer than the initial timeout we'd set. We ended up having to increase the NER request budget considerably.

We gave up some extension size and responsiveness in exchange for keeping inference on the user's device. We considered that a worthwhile trade.

6. The NER Debugging War

The NER model was probably the most interesting part of the entire build. It was also where we spent the most time debugging things that looked completely reasonable on paper.

The title-casing trap

Our first implementation title-cased the entire prompt before sending it to the model. The thinking was: if the model recognizes names better when capitalized, why not normalize the input? It made things worse. The transformation distorted sentence context and caused names to be misclassified as organizations or locations.

So we removed the transformation. That was logically cleaner. It also broke lowercase names completely — the model is cased, trained on data where entities were almost always capitalized. The eventual fix was a split-brain approach:

Original text ──────────────────→ Output / token values
      │
      ▼
Inference copy (title-cased)
      │
      ▼
NER model

Because word-initial title-casing is a 1:1 character replacement, offsets in both strings are identical. We run inference on the transformed copy; every span maps straight back to the original.

The B-PER merge that swallowed sentences

Our first entity-merging logic treated adjacent B-PER tokens as though they could belong to the same entity. Combined with the fact that Transformers.js omits O-labeled tokens from its output entirely, the index gaps between entities were invisible. The result:

"James Martins and Femi Balogun" → ONE PER span

The fix: a B- label always starts a new entity boundary, and token index gaps flush the current span.

The off-by-one that produced a{ORG_1}talking

A low-confidence head token was dropped, so a subword token shifted the reconstructed span, and an indexOf search found the wrong occurrence. The fix: never drop ##subword tokens from a group, and trim span boundaries against the actual source text rather than trusting the decoded length.

When better recall created worse precision

Once recall improved, the model started producing things like James Martins tomorrow as a single PERSON entity — words like “tomorrow” and “will” were being pulled in. We added a COMMON_WORDS filter: common English words can never start or extend a person span.

7. The Model Has Limits

Eventually we benchmarked multiple browser-compatible NER models against real examples from our test set. The result was uncomfortable but useful.

The models could recognize names like femi balogun when presented in isolation. Inside a long mixed prompt, the same name could be classified incorrectly as a location or organization. No model we tested reliably solved the problem in every context.

You need to know the difference between a bug and a model limitation. A span-boundary bug is an engineering problem. A model struggling with a particular context is a capability problem. The first should be fixed. The second needs to be engineered around.

So instead of endlessly swapping models, we kept NER as one layer and added deterministic contextual fallbacks for situations where the surrounding language provided a strong enough signal. That is how the hybrid detector emerged.

8. The Cache Bug That Made Everything Look Broken

One of the subtlest bugs wasn't in the model at all. When the model was cold and inference timed out, the adapter cached the result as an empty array — []. The next time the exact same text was processed, the cache returned “no entities found” even though the model was now available. A temporary failure had become a permanent-looking detection failure.

The fix was simple: only cache successful inference results, and make a hard distinction between NER found nothing and NER wasn't available. Those are two completely different states that must never be treated as the same thing.

9. The International PII Problem

While we were solving names and organizations, another weakness became obvious: our structured detectors were entirely built around UK formats.

A UK postcode looks like SW1A 1AA. A Nigerian postal code is six digits. A US ZIP is five. A Canadian postal code alternates letters and digits. A UK phone number starts with +44 or 07. A Nigerian mobile starts with 080, 081, 090. None of those matched our patterns.

This wasn't a machine-learning problem. It was a product assumption that had leaked into the engineering — we had built a structured detection layer for one geography and treated it as universal. Structured PII still has patterns; we simply needed to support those patterns across the regions where ARKN actually operates.

10. There Was an Entire Product Around the Detector

It would be easy to tell this story as though ARKN is just a detection engine. It isn't. The detector is only one part: authentication, organizations, memberships, policies, device state, telemetry, dashboards, extension updates, and the communication between the extension and the web application all needed building and maintaining.

Even authentication turned into its own architecture problem. The extension stores its session in browser storage and proactively refreshes it every 50 minutes. The dashboard stores its session in Supabase SSR cookies. At one point dashboard sessions weren't being refreshed correctly across visits, while extension refresh-token rotation could leave devices appearing offline even though the user was technically still signed in.

The hard part of a security product isn't just the security mechanism. It's everything around it that has to remain reliable.

11. And Then Chrome Rejected Us

After all that work, we submitted the extension to the Chrome Web Store. Chrome rejected it. The reason?

Could not decode image: 'icon.svg'

Chrome's extension runtime doesn't accept SVG for manifest icon fields. The extension worked locally, and the developer interface rendered the icon fine, so we hadn't caught it before submission.

The fix was embarrassingly simple: generate PNG versions of the icon. The slightly ridiculous part was that our extension had no build dependency for rasterizing SVGs, so rather than introduce one for a one-time conversion, we wrote a small PNG encoder in Node.js using only the built-in zlib module — raw RGBA buffers, hand-written PNG chunks, CRC calculations, deflated scanlines. The resulting icons were 125, 234, and 666 bytes respectively.

It was a five-minute problem hidden behind hours of other engineering. And that's software.

12. The Trade-offs

At this point, ARKN is a collection of deliberate trade-offs:

  • Local NER over a remote API — we get privacy; we give up extension size and cold-start latency.
  • Token-classification over a large language model — browser inference becomes possible; contextual reasoning suffers.
  • Layered detection over a single universal model — each layer covers the others' blind spots; complexity increases.
  • MAIN-world interception — we can see the network boundary; isolation is reduced.
  • Local token maps — the privacy guarantee is architectural, not a promise; lifecycle management is more complex.

The trade-offs we'd make differently: we would design the pipeline as hybrid from day one, scope structured detectors internationally from the start, and write a live-model test harness before any adapter code. The rest we'd make again.

13. Building It With AI Agents

A large part of the engineering process happened alongside AI coding agents. That doesn't mean we described the product and watched an AI build a security platform for us. It was much messier.

The agent could write code quickly, help reason through implementations, generate adapters, refactor detectors, write tests, and work through repetitive changes. But when the system behaved incorrectly, we still had to understand what was actually happening. The NER debugging process was the clearest example — it was tempting to keep asking the agent what was wrong. The better approach was to run the real model against the real text and inspect the raw output.

AI makes implementation dramatically faster. It doesn't remove the need to understand the system. If anything, it makes understanding more important — because you can generate plausible implementations much faster than you can validate them.

14. What We Learned

  • The model is a black box until you run it. Empiricism beats intuition every time. Run the actual model on the actual text.
  • Bugs and capability ceilings are different things. Stop trying to fix model limitations with more debugging.
  • Test where the bugs actually live. Our pipeline tests mocked the NER worker perfectly while missing every bug inside it.
  • Never cache failure as success. “No entities” and “inference failed” are different states.
  • Transformations need to preserve what you need to recover. Transform only the inference copy; keep the original untouched.
  • Silent failures are dangerous. Security software needs observable failure modes.

15. What Exists Today — and What Comes Next

We have a working browser-first privacy layer. A Chrome extension sits between the user and supported AI platforms. Structured PII is detected deterministically. Semantic entities are caught by local NER. Organization-specific rules can be applied. Everything is merged into a single protection pipeline and the originals never leave the device.

But I don't want to oversell what that means. The NER model is not perfect. There are still false positives and false negatives. International structured PII coverage needs to grow. Local inference still has startup costs. There are entire categories of sensitive information we haven't solved yet.

The objective of this version wasn't to solve every problem. It was to prove something more fundamental:

Can we build a useful security boundary between people and AI without requiring their private information to pass through our own infrastructure?

We believe the answer is yes. The model will get smaller. Inference will get faster. Detection will get better. What exists today is not the finished product. It's the foundation. And honestly, that's the part we're most excited about.