Rampart is a local-first system for removing personally identifiable information from user-typed text before it leaves the browser. It combines a 14.7 MB ONNX token-classification model with a deterministic recognizer layer; together they form a defense-in-depth pipeline released as a complete, reproducible artifact.
This repository ships the runtime as @nationaldesignstudio/rampart.
Model weights load from Hugging Face by default; a copy also lives in model/ for
local serving and training.
"My name is Alex Rivera and my SSN is 472-81-0094."
→ "My name is [GIVEN_NAME_1] [SURNAME_1] and my SSN is [SSN_1]."
The model provider sees placeholders. The user sees restored values on the client. The session table never leaves the device.
Rampart is harm reduction, not perfect protection. Personal data the client fails to redact is the upper bound on what can leak through a model provider, a logging pipeline, or a future infrastructure compromise. No detector at this size catches everything; we document failure modes openly and ship regression tests so future training runs surface drops immediately.
See WHITEPAPER.md for the full technical writeup — methodology, candidate sweep, calibration, schema reconciliation, and reproducibility.
This release supports seven Latin-script languages: English, Spanish, French, German, Italian, Portuguese, and Dutch. Every headline number below is measured on these languages.
Text and names in non-Latin scripts (e.g. Chinese, Japanese, Korean, Arabic, Cyrillic, Devanagari) are out of scope in this release: recall drops sharply and the system should not be relied on for them. See Limitations.
On a 30,000-row held-out test set spanning all seven supported languages from the OpenPII 1.5M dataset, the full system (model + deterministic layer + policy) achieves:
| Metric | Value |
|---|---|
| Private-term recall (7 languages) | 98.42% (Wilson 95% CI [98.35, 98.49]) |
| Public-term retention | 91.7% term-presence (>99% policy-aware*) |
| Latency p50 (Node ONNX) | 6.6 ms† |
| Shipped artifact size | 14.7 MB Q4 ONNX (≈15.0 MB with tokenizer) |
* OpenPII marks street-line components as public; Rampart redacts the precise
street line (BUILDING_NUMBER + STREET_NAME) and the secondary-address line
(SECONDARY_ADDRESS) while keeping city, state, and ZIP.
See "Schema reconciliation" in the whitepaper.
† Latency is hardware-dependent; the committed eval/bench/runs/sample-900 proof run
records ≈14 ms p50 on CI hardware. Over the held-out slice the browser pipeline runs at
3.9 ms p50 on WebGPU (Apple Metal) and 12.6 ms on WASM via bun run bench:webgpu.
These numbers come from the shipped Q4 pipeline scored end-to-end by the committed
eval/bench harness on a pinned held-out slice. The harness was corrected since the
previous revision: city/state/ZIP are now scored as kept (matching the runtime policy)
rather than counted as leaks, which is why public retention rose to ~90% while the
headline recall is reported against the larger, harder seven-language slice.
Per supported language, on the same 30,000-row test set:
| Language | Private recall | Public retention |
|---|---|---|
| English (en) | 98.85% | 90.5% |
| Spanish (es) | 98.84% | 91.6% |
| French (fr) | 98.41% | 92.8% |
| German (de) | 97.94% | 91.7% |
| Italian (it) | 97.83% | 94.1% |
| Portuguese (pt) | 97.73% | 92.5% |
| Dutch (nl) | 97.21% | 91.9% |
On the English+Spanish slice the full system scores 98.85% recall — the slice used for the model-selection sweep in the whitepaper.
Two cooperating layers run in parallel and merge their outputs. Both run entirely in the browser.
Regular expressions paired with checksum and structural validators. It owns five classes end-to-end:
000, 666, 9xx) and
ZIP+4-style false positives.This layer is synchronous and runs before the model loads; its spans are masked to sentinels so the model never re-derives them. Names, phone numbers, account and routing numbers, government identifiers, passports, licenses, and street-address components carry no checksum, so they are left to the model rather than guessed at with a regex.
A MiniLM-L6-H384 encoder fine-tuned on a 35-label BIO head (17 entity types) covers
contextual PII the regex layer can't checksum — split names (GIVEN_NAME,
SURNAME), phone numbers, account/routing/tax numbers, government IDs, passports,
licenses, and free-form address components — across seven Latin-script languages
(en, es, fr, de, it, pt, nl). Vocabulary is trimmed to 19,730 WordPieces;
single-character pieces are retained for rare-name fallback.
Span repair (adjacent merge, bridge-and-merge, capitalized-particle rescue) lifts span-F1 to 0.53 strict (IoU=1.0) and 0.66 relaxed (IoU≥0.5) on the headline test set — well above the fragmented spans HuggingFace's default aggregation produces for subword-split names.
Default-deny policy: every detected label is redacted unless explicitly kept.
The default keep-set is {CITY, STATE, ZIP_CODE} — coarse geography an assistant
can reason about — while the precise street line (BUILDING_NUMBER + STREET_NAME)
and the secondary-address line (SECONDARY_ADDRESS) are always redacted.
Session table: maps each raw value to a stable placeholder (Maria Garcia → [GIVEN_NAME_1] [SURNAME_1]).
Placeholders are restored locally in assistant responses. The table is never transmitted.
The npm package exposes a single entry point — createGuard() returns a ChatGuard
that runs the full pipeline (detect → policy → placeholders) and keeps per-conversation
state for reveal().
npm install @nationaldesignstudio/rampart @huggingface/transformers
@huggingface/transformers is a peer dependency — your app bundles and serves it.
Create one ChatGuard per conversation. createGuard() loads the shipped q4 classifier
(nationaldesignstudio/rampart) from Hugging Face by default, caches it on-device, and
pairs it with the deterministic layer.
import { createGuard } from "@nationaldesignstudio/rampart";
const guard = await createGuard();
// Scrub the user message before it reaches your LLM or server.
const safe = await guard.protect(userMessage);
// Send safe.text — placeholders, not raw PII — to the model.
const reply = await llm(safe.text);
// Restore real values before showing the reply to the user.
guard.reveal(reply);
Streaming replies: stream.pipeThrough(guard.revealTransform()).
Scrub model output before logging: await guard.protectReply(reply).
| Option | Default | Purpose |
|---|---|---|
model | nationaldesignstudio/rampart | Hugging Face model id or local ONNX directory |
device | "wasm" | "wasm" / "webgpu" in browsers; "cpu" in Node |
worker | — | Worker script URL — run NER off the main thread |
heuristicsOnly | false | Skip the classifier; structured PII only |
keepLabels | city, state, ZIP | Widen or narrow the default-deny keep-set |
aliases | {} | Display names for tokens, e.g. { GIVEN_NAME: "NAME" } |
ner | — | Inject a custom detector; skips model |
minScore | 0.4 | Drop model spans below this confidence |
noPrefilter | false | Feed raw text to the model (no-prefilter ablation); heuristics still run |
// Local weights (e.g. self-hosted or repo `model/` dir)
const guard = await createGuard({ model: "./model", device: "cpu" });
// Heuristics only — no model load
const guard = await createGuard({ heuristicsOnly: true });
// Keep inference off the UI thread (browser)
const guard = await createGuard({
worker: new URL("./pii-worker.ts", import.meta.url),
});
Custom models must be token-classification ONNX exports (q4) with a label schema compatible with Rampart. Dtype is fixed to q4.
Set HF_TOKEN when pulling from a private Hugging Face repo.
bun run redact # interactive terminal redactor (Node, device: cpu)
The most consequential documented gaps:
See MODEL_CARD.md for per-class statistics and failure modes.
Everything is evaluated in TypeScript against the shipped ChatGuard pipeline — the
same code consumers run is the code under test:
eval/bench) — runs the real
@nationaldesignstudio/rampart pipeline over a frozen OpenPII held-out slice and
scores recall/retention (Wilson CI), span-F1, and latency. The headline numbers
above are regenerated by this harness.bun test # unit + public API suites
bun run eval:public # end-to-end chat cases
bun run bench:fetch --n 30000 # materialise the held-out slice
bun run bench # score the shipped pipeline
| Document | Contents |
|---|---|
| WHITEPAPER.md | Full technical writeup |
| MODEL_CARD.md | Model summary, training data, eval results, limitations |
| RELEASE.md | Verify and publish checklist |
| Channel | Artifact |
|---|---|
| npm | @nationaldesignstudio/rampart — TypeScript runtime API |
| GitHub | nationaldesignstudio/rampart — source, tests, eval harness, model weights |
Released under CC BY 4.0 (Creative Commons Attribution 4.0 International).
Training data: OpenPII 1.5M (CC BY 4.0).
87 followers · starred Jul 2026
241 followers · starred Jul 2026
94 followers · starred Jun 2026
193 followers · starred Jun 2026
Rampart is a local-first system for removing personally identifiable information from user-typed text before it leaves the browser. It combines a 14.7 MB ONNX token-classification model with a deterministic recognizer layer; together they form a defense-in-depth pipeline released as a complete, reproducible artifact.
This repository ships the runtime as @nationaldesignstudio/rampart.
Model weights load from Hugging Face by default; a copy also lives in model/ for
local serving and training.
"My name is Alex Rivera and my SSN is 472-81-0094."
→ "My name is [GIVEN_NAME_1] [SURNAME_1] and my SSN is [SSN_1]."
The model provider sees placeholders. The user sees restored values on the client. The session table never leaves the device.
Rampart is harm reduction, not perfect protection. Personal data the client fails to redact is the upper bound on what can leak through a model provider, a logging pipeline, or a future infrastructure compromise. No detector at this size catches everything; we document failure modes openly and ship regression tests so future training runs surface drops immediately.
See WHITEPAPER.md for the full technical writeup — methodology, candidate sweep, calibration, schema reconciliation, and reproducibility.
This release supports seven Latin-script languages: English, Spanish, French, German, Italian, Portuguese, and Dutch. Every headline number below is measured on these languages.
Text and names in non-Latin scripts (e.g. Chinese, Japanese, Korean, Arabic, Cyrillic, Devanagari) are out of scope in this release: recall drops sharply and the system should not be relied on for them. See Limitations.
On a 30,000-row held-out test set spanning all seven supported languages from the OpenPII 1.5M dataset, the full system (model + deterministic layer + policy) achieves:
| Metric | Value |
|---|---|
| Private-term recall (7 languages) | 98.42% (Wilson 95% CI [98.35, 98.49]) |
| Public-term retention | 91.7% term-presence (>99% policy-aware*) |
| Latency p50 (Node ONNX) | 6.6 ms† |
| Shipped artifact size | 14.7 MB Q4 ONNX (≈15.0 MB with tokenizer) |
* OpenPII marks street-line components as public; Rampart redacts the precise
street line (BUILDING_NUMBER + STREET_NAME) and the secondary-address line
(SECONDARY_ADDRESS) while keeping city, state, and ZIP.
See "Schema reconciliation" in the whitepaper.
† Latency is hardware-dependent; the committed eval/bench/runs/sample-900 proof run
records ≈14 ms p50 on CI hardware. Over the held-out slice the browser pipeline runs at
3.9 ms p50 on WebGPU (Apple Metal) and 12.6 ms on WASM via bun run bench:webgpu.
These numbers come from the shipped Q4 pipeline scored end-to-end by the committed
eval/bench harness on a pinned held-out slice. The harness was corrected since the
previous revision: city/state/ZIP are now scored as kept (matching the runtime policy)
rather than counted as leaks, which is why public retention rose to ~90% while the
headline recall is reported against the larger, harder seven-language slice.
Per supported language, on the same 30,000-row test set:
| Language | Private recall | Public retention |
|---|---|---|
| English (en) | 98.85% | 90.5% |
| Spanish (es) | 98.84% | 91.6% |
| French (fr) | 98.41% | 92.8% |
| German (de) | 97.94% | 91.7% |
| Italian (it) | 97.83% | 94.1% |
| Portuguese (pt) | 97.73% | 92.5% |
| Dutch (nl) | 97.21% | 91.9% |
On the English+Spanish slice the full system scores 98.85% recall — the slice used for the model-selection sweep in the whitepaper.
Two cooperating layers run in parallel and merge their outputs. Both run entirely in the browser.
Regular expressions paired with checksum and structural validators. It owns five classes end-to-end:
000, 666, 9xx) and
ZIP+4-style false positives.This layer is synchronous and runs before the model loads; its spans are masked to sentinels so the model never re-derives them. Names, phone numbers, account and routing numbers, government identifiers, passports, licenses, and street-address components carry no checksum, so they are left to the model rather than guessed at with a regex.
A MiniLM-L6-H384 encoder fine-tuned on a 35-label BIO head (17 entity types) covers
contextual PII the regex layer can't checksum — split names (GIVEN_NAME,
SURNAME), phone numbers, account/routing/tax numbers, government IDs, passports,
licenses, and free-form address components — across seven Latin-script languages
(en, es, fr, de, it, pt, nl). Vocabulary is trimmed to 19,730 WordPieces;
single-character pieces are retained for rare-name fallback.
Span repair (adjacent merge, bridge-and-merge, capitalized-particle rescue) lifts span-F1 to 0.53 strict (IoU=1.0) and 0.66 relaxed (IoU≥0.5) on the headline test set — well above the fragmented spans HuggingFace's default aggregation produces for subword-split names.
Default-deny policy: every detected label is redacted unless explicitly kept.
The default keep-set is {CITY, STATE, ZIP_CODE} — coarse geography an assistant
can reason about — while the precise street line (BUILDING_NUMBER + STREET_NAME)
and the secondary-address line (SECONDARY_ADDRESS) are always redacted.
Session table: maps each raw value to a stable placeholder (Maria Garcia → [GIVEN_NAME_1] [SURNAME_1]).
Placeholders are restored locally in assistant responses. The table is never transmitted.
The npm package exposes a single entry point — createGuard() returns a ChatGuard
that runs the full pipeline (detect → policy → placeholders) and keeps per-conversation
state for reveal().
npm install @nationaldesignstudio/rampart @huggingface/transformers
@huggingface/transformers is a peer dependency — your app bundles and serves it.
Create one ChatGuard per conversation. createGuard() loads the shipped q4 classifier
(nationaldesignstudio/rampart) from Hugging Face by default, caches it on-device, and
pairs it with the deterministic layer.
import { createGuard } from "@nationaldesignstudio/rampart";
const guard = await createGuard();
// Scrub the user message before it reaches your LLM or server.
const safe = await guard.protect(userMessage);
// Send safe.text — placeholders, not raw PII — to the model.
const reply = await llm(safe.text);
// Restore real values before showing the reply to the user.
guard.reveal(reply);
Streaming replies: stream.pipeThrough(guard.revealTransform()).
Scrub model output before logging: await guard.protectReply(reply).
| Option | Default | Purpose |
|---|---|---|
model | nationaldesignstudio/rampart | Hugging Face model id or local ONNX directory |
device | "wasm" | "wasm" / "webgpu" in browsers; "cpu" in Node |
worker | — | Worker script URL — run NER off the main thread |
heuristicsOnly | false | Skip the classifier; structured PII only |
keepLabels | city, state, ZIP | Widen or narrow the default-deny keep-set |
aliases | {} | Display names for tokens, e.g. { GIVEN_NAME: "NAME" } |
ner | — | Inject a custom detector; skips model |
minScore | 0.4 | Drop model spans below this confidence |
noPrefilter | false | Feed raw text to the model (no-prefilter ablation); heuristics still run |
// Local weights (e.g. self-hosted or repo `model/` dir)
const guard = await createGuard({ model: "./model", device: "cpu" });
// Heuristics only — no model load
const guard = await createGuard({ heuristicsOnly: true });
// Keep inference off the UI thread (browser)
const guard = await createGuard({
worker: new URL("./pii-worker.ts", import.meta.url),
});
Custom models must be token-classification ONNX exports (q4) with a label schema compatible with Rampart. Dtype is fixed to q4.
Set HF_TOKEN when pulling from a private Hugging Face repo.
bun run redact # interactive terminal redactor (Node, device: cpu)
The most consequential documented gaps:
See MODEL_CARD.md for per-class statistics and failure modes.
Everything is evaluated in TypeScript against the shipped ChatGuard pipeline — the
same code consumers run is the code under test:
eval/bench) — runs the real
@nationaldesignstudio/rampart pipeline over a frozen OpenPII held-out slice and
scores recall/retention (Wilson CI), span-F1, and latency. The headline numbers
above are regenerated by this harness.bun test # unit + public API suites
bun run eval:public # end-to-end chat cases
bun run bench:fetch --n 30000 # materialise the held-out slice
bun run bench # score the shipped pipeline
| Document | Contents |
|---|---|
| WHITEPAPER.md | Full technical writeup |
| MODEL_CARD.md | Model summary, training data, eval results, limitations |
| RELEASE.md | Verify and publish checklist |
| Channel | Artifact |
|---|---|
| npm | @nationaldesignstudio/rampart — TypeScript runtime API |
| GitHub | nationaldesignstudio/rampart — source, tests, eval harness, model weights |
Released under CC BY 4.0 (Creative Commons Attribution 4.0 International).
Training data: OpenPII 1.5M (CC BY 4.0).
87 followers · starred Jul 2026
241 followers · starred Jul 2026
94 followers · starred Jun 2026
193 followers · starred Jun 2026