A small, inspectable FastAPI service and Next.js sandbox for running
openai/privacy-filter. It detects privacy-related
spans in text, applies configurable redaction, and exposes a minimal API that can be deployed behind
a server-side web proxy.
This project is intentionally narrow: it is a testable wrapper around a privacy-detection model, not a production policy engine, classifier benchmark, or complete data governance system.
- FastAPI API with
/healthand/v1/filter. - Next.js sandbox UI for testing sample text and inspecting detected spans.
@shiftbloom-studio/privacy-filter: the in-browser engine as a standalone npm package (Transformers.js, WebGPU/WASM) for reuse in other Next.js apps.- Two runtimes behind one contract: in-browser (WebGPU/WASM) and server-side (FastAPI).
- Redaction modes:
mask,remove, andannotate. - Optional internal-token protection between the web proxy and API.
- Docker images for the API and web app.
- Consent gate before the ~900 MB in-browser model download.
- Vercel configuration for the no-compute browser-runtime deployment.
- AWS App Runner workflow, retained for self-hosters with inference compute.
- Unit tests for schemas, redaction, API behavior, Lambda adapter, web proxy, and UI behavior.
- Model files are kept outside source control and can be restored from deployment artifacts.
The sandbox supports two runtimes selected with NEXT_PUBLIC_PRIVACY_FILTER_RUNTIME:
browser(default): the model runs in the visitor's browser through Transformers.js, using WebGPU where available and falling back to WASM (CPU). The engine lives in the standalone@shiftbloom-studio/privacy-filterpackage. No inference compute is required and input text never leaves the device. Because the weights are about 900 MB, the sandbox asks for consent before downloading anything — nothing is fetched until the visitor agrees.server: the Next.js server route proxies to the Python FastAPI service.
flowchart LR
User["Browser"] --> Web["Next.js sandbox"]
Web -->|"runtime: browser"| InBrowser["Transformers.js WebGPU/WASM"]
InBrowser --> RedactionB["Redaction logic"]
Web -->|"runtime: server"| Proxy["/api/filter server route"]
Proxy --> API["FastAPI /v1/filter"]
API --> Model["openai/privacy-filter"]
API --> RedactionS["Redaction logic"]
Both runtimes share the same request and response contract and the same redaction semantics, so the sandbox behaves identically either way.
The Shiftbloom deployment on Vercel (privacy.shiftbloom.studio) runs the browser runtime because
no server-side inference compute is provisioned. The API, Lambda, and Cloud Run paths remain fully
maintained for self-hosters — they are simply not deployed for Shiftbloom. See
docs/deployment.md.
In server mode the browser never calls the model API directly. The Next.js app proxies requests
through its server-side route, optionally adding PRIVACY_FILTER_INTERNAL_TOKEN so the API can
reject direct public traffic.
apps/
api/ FastAPI service, Lambda adapter, redaction logic, and tests
web/ Next.js App Router sandbox, API proxy, and tests
packages/
privacy-filter/ @shiftbloom-studio/privacy-filter — in-browser engine (npm package)
infra/
docker/ API and web Dockerfiles
docs/ API contract and deployment notes
The in-browser engine is published to npm as
@shiftbloom-studio/privacy-filter from this repository (on GitHub Release, via
publish-package.yml), so other Next.js (or any
bundler-based) apps can run the same model client-side:
npm install @shiftbloom-studio/privacy-filter"use client";
import { applyRedaction, detectSpansInBrowser } from "@shiftbloom-studio/privacy-filter";
const spans = await detectSpansInBrowser(text, { onProgress: setProgress });
const [filtered] = applyRedaction(text, spans, "mask", "[REDACTED]");Detection is model-only (no regex/keyword heuristics), long text is chunked for the model's 257-token window, and the ~900 MB download starts only when you call the engine — gate it behind a consent prompt. See the package README for the full API.
- Python 3.11 or newer
- Node.js 24 or newer
- npm
- Docker, optional but recommended for deployment parity
- AWS CLI, optional: only for the retained App Runner and Lambda paths. Not needed for the Vercel deployment or for local browser-runtime development.
The real model runtime requires torch and transformers. Unit tests do not download or load the
model unless the explicit real-model smoke test is enabled.
Running the sandbox in the browser requires no Python inference dependencies at all.
Clone the repo and copy the example environment:
git clone https://github.com/shiftbloom-studio/openai-privacy-filter-api.git
cd openai-privacy-filter-api
cp .env.example .envInstall web dependencies and start the sandbox:
npm install
npm --workspace apps/web run devOpen http://localhost:3000. Before the first filter runs, the sandbox asks for consent and then
downloads the model weights — roughly 900 MB (~874 MB of q4 ONNX weights plus a ~26 MB tokenizer)
— which the browser caches for later visits.
Start the API without inference dependencies:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e "apps/api[dev]"
uvicorn privacy_filter_api.main:app --app-dir apps/api/src --reloadStart the web sandbox in another shell:
npm install
NEXT_PUBLIC_PRIVACY_FILTER_RUNTIME=server \
PRIVACY_FILTER_API_URL=http://localhost:8000 npm --workspace apps/web run devOpen http://localhost:3000.
Install inference dependencies:
source .venv/bin/activate
python -m pip install -e "apps/api[dev,inference]"Run the API with a Hugging Face cache directory:
HF_HOME=.hf-cache uvicorn privacy_filter_api.main:app --app-dir apps/api/src --reloadThe first request that needs inference may download model files. To run the smoke test explicitly:
RUN_REAL_MODEL_TESTS=1 python -m pytest apps/api/tests/test_real_model_smoke.pyHealth check:
curl http://localhost:8000/healthFilter text:
curl -X POST http://localhost:8000/v1/filter \
-H "content-type: application/json" \
-d '{
"text": "My name is Alice Smith and my email is alice@example.com.",
"mode": "mask",
"mask_token": "[REDACTED]",
"include_spans": true
}'Example response:
{
"original_text": "My name is Alice Smith and my email is alice@example.com.",
"filtered_text": "My name is [REDACTED] and my email is [REDACTED].",
"spans": [
{
"label": "private_person",
"start": 11,
"end": 22,
"text": "Alice Smith",
"score": 0.97
}
],
"model": "openai/privacy-filter"
}Supported labels:
account_numberprivate_addressprivate_emailprivate_personprivate_phoneprivate_urlprivate_datesecret
Supported modes:
mask: replace each accepted span withmask_token.remove: remove each accepted span.annotate: replace each accepted span with[label:value].
See docs/api.md for the API contract.
| Variable | Used by | Default | Description |
|---|---|---|---|
PRIVACY_FILTER_MODEL_ID |
API | openai/privacy-filter |
Hugging Face model id reported by the service and used when no model path is set. |
PRIVACY_FILTER_MODEL_PATH |
API | empty | Local model directory. Use this for baked or mounted model files. |
PRIVACY_FILTER_RUNTIME |
API | local |
Runtime label returned by /health. |
PRIVACY_FILTER_CORS_ORIGINS |
API | http://localhost:3000,https://privacy.shiftbloom.studio |
Comma-separated CORS allowlist. |
PRIVACY_FILTER_INTERNAL_TOKEN |
API and web | empty | Optional shared token. The web proxy sends it to the API. |
PRIVACY_FILTER_DEVICE |
API | empty | Optional Transformers device setting. |
PRIVACY_FILTER_REVISION |
API | empty | Optional Hugging Face model revision. |
PRIVACY_FILTER_TRUST_REMOTE_CODE |
API | false |
Enables remote model code if a future revision requires it. |
HF_HOME |
API | .hf-cache locally |
Hugging Face cache directory. |
PRIVACY_FILTER_API_URL |
web | http://localhost:8000 |
API base URL used by the Next.js server-side proxy. Only used in server runtime. |
NEXT_PUBLIC_PRIVACY_FILTER_RUNTIME |
web | browser |
browser runs the model in-browser (WebGPU/WASM). server proxies to the FastAPI service. |
API:
source .venv/bin/activate
python -m ruff check apps/api
python -m pytest apps/apiWeb and package (root scripts cover both apps/web and packages/privacy-filter):
npm run lint
npm run typecheck
npm test
npm run buildOptional real-model smoke test for the browser engine (downloads the actual ~900 MB weights once, then uses the local Hugging Face cache):
npm run verify:modelDocker smoke build:
docker build -f infra/docker/api.Dockerfile --build-arg API_EXTRAS= -t privacy-filter-api:core .
docker build -f infra/docker/web.Dockerfile -t privacy-filter-web .Run both services locally with Docker Compose:
docker compose up --buildBuild the API image:
docker build -f infra/docker/api.Dockerfile -t privacy-filter-api .Build the web image:
docker build -f infra/docker/web.Dockerfile -t privacy-filter-web .The API Dockerfile copies privacy-filter-model/ into /models/privacy-filter. For production
offline inference, place the required model files there before building and set
PRIVACY_FILTER_MODEL_PATH=/models/privacy-filter.
Required model files:
config.jsonmodel.safetensorstokenizer.jsontokenizer_config.jsonviterbi_calibration.json
Do not commit model files to the repository.
The project can run anywhere that supports Docker containers. The included deployment notes cover Vercel, Docker, AWS Lambda container images, Google Cloud Run, Cloudflare routing, and the AWS App Runner setup that remains available to self-hosters. See docs/deployment.md.
privacy.shiftbloom.studio is deployed on Vercel and runs the browser runtime: the model
executes in the visitor's browser, so no server-side inference compute is required or provisioned.
The repository ships a vercel.json for this setup. Configure the Vercel project with:
| Setting | Value |
|---|---|
| Root Directory | apps/web |
| Framework Preset | Next.js |
| Build Command | cd ../.. && npm run build |
| Install Command | cd ../.. && npm install |
| Output Directory | .next |
NEXT_PUBLIC_PRIVACY_FILTER_RUNTIME is pinned to browser in vercel.json, so no environment
variable is strictly required. It is inlined at build time, so changing it needs a redeploy rather
than a restart.
Because the weights are roughly 900 MB, the sandbox asks for consent before downloading anything. Nothing is fetched on page load.
The repository also includes a GitHub Actions workflow for App Runner, retained for operators who have provisioned inference compute. It is not part of the Shiftbloom deployment and is currently disabled:
- API App Runner service:
privacy-filter-api - Web App Runner service:
privacy-filter-web - AWS region:
eu-central-1 - ECR repositories:
privacy-filter-api,privacy-filter-web - Model artifact bucket:
shiftbloom-privacy-filter-build-349744179866-eu-central-1
On pushes to main, deploy-aws.yml detects changed paths and
deploys only the affected surface. It can also be run manually with all, api, web, or auto.
Forks should replace the AWS account id, service ARNs, ECR repositories, artifact bucket, and OIDC role with their own infrastructure.
- Treat model output as advisory. Validate behavior against your data and policy requirements.
- Do not send sensitive production data to infrastructure you do not control.
- Use
PRIVACY_FILTER_INTERNAL_TOKENwhen the API is reachable outside a private network. - Keep CORS origins narrow in production.
- Keep model files, caches, credentials, and deployment artifacts out of source control.
- Review upstream model terms and dependencies before production use.
Contributions are welcome. Keep changes focused and include tests for behavior changes.
Suggested flow:
- Open an issue or draft PR for larger changes.
- Run the relevant verification commands locally.
- Keep API contract changes documented in docs/api.md.
- Keep deployment changes documented in docs/deployment.md.
Please avoid committing generated model files, local caches, credentials, or machine-specific build artifacts.
Apache License 2.0. See LICENSE.
This project wraps openai/privacy-filter through
standard Python and web application tooling. Model behavior, supported labels, and runtime
requirements may change with upstream model revisions.