ADR 004 — Detection: Hybrid Go-native + Python Presidio Sidecar
Status: Accepted Date: 2026-04-23 Deciders: Jerome Related: prd-detection-tiering, detection-multitier-architecture, nlp-strategy, ml-benchmark-plan, onnx-pipeline
Context
Current detection stack is pure Go (noxys-api/internal/classification/). This covers structured PII (NIR, IBAN, BIC, RIB, RPPS, FINESS, Carte Vitale, LEI, AWS/Slack/Anthropic tokens…) very well: RE2 is linear-time, zero ReDoS risk, <1 ms per pattern match, trivially deployable, memory-cheap.
Two problems push us beyond pure Go:
- No NER in Go. Go has no mainstream, battle-tested NER library. Embedding a transformer in Go via ONNX runtime is technically possible but the tokenizer + post-processing ecosystem is immature compared to Python. Spacy + Presidio is the de-facto standard.
- Multi-language EU expansion. DE/ES/IT/NL structured IDs can be added in Go (regex + context). But the free-text NER (person names, addresses, DOB in prose) requires language-specific models. Five of those is spaCy territory.
We must also ship a moat between Starter (8€/u) and Business (15€/u) tiers. "Better NER" is a natural, defensible tier-2 feature.
Decision
Hybrid architecture. Three detection layers composed per tier.
- Go-native structured PII, all tiers. Existing Go recognizers + new
recognizers_de.go/_es.go/_it.go/_nl.go. Fast, cheap, no ML runtime. Covers 80% of CISO use cases. - CamemBERT ONNX in Web Worker, Starter+. FR-only NER, runs in the browser extension, ~45 MB INT8 model, WASM+SIMD backend. Solves FR free-text detection without introducing a server-side ML runtime for Starter customers. <15 ms p95.
- Python Presidio sidecar, Business+. Full 5-lang NER (fr/de/es/it/nl spaCy lg), Presidio recognizer engine, custom recognizer CRUD, tenant-aware cache. Separate process, mTLS + JWT, gRPC from noxys-api.
noxys-api composes them: always runs Go recognizers; if user's tier includes sidecar_access and sidecar is healthy, it calls sidecar and merges entity spans; deduplicates by span overlap + higher score wins.
Consequences
Positive
- Starter gets real FR NER without any server-side Python runtime — keeps Starter cost low, protects margin
- Business tier has a clear technical moat (5-lang server NER + custom recognizer API)
- Graceful degradation: sidecar outage falls back to Go-only with a counter + alert
- Python stays isolated in its own repo / container / team — no Python in noxys-api
- CI/CD independent (sidecar ships on its own cadence)
- Air-gap story tractable: one Go binary + one signed Python container
Negative
- Two runtimes to maintain. Go + Python. Ops complexity up. Mitigated by treating sidecar as a black-box service with a stable gRPC contract.
- Distributed tracing needed. Request now spans two services. We must correlate via
request_idin gRPC metadata and propagate OpenTelemetry. - Latency penalty. Sidecar hop adds ~20–60 ms p95. Acceptable inside our 250 ms end-to-end budget for Business tier.
- RAM cost. 5 spaCy lg models + Presidio engines ≈ 2 GB. Mitigated by LRU eviction + lazy load.
- Quantization risk. CamemBERT INT8 may drop F1 by 1–3%. Benchmark in DT-2, accept if drop <2%, fallback to FP16 otherwise.
Operational
- New repo
noxys-presidiowith its own CI, versioning, release cadence - Noxys-api gains a Go gRPC client + circuit breaker (DT-4)
- Helm chart adds a new deployment + NetworkPolicy
- PlanGate gains
sidecar_accessfeature flag (DT-6)
Alternatives Considered
Alternative A — Go-only forever (add DE/ES/IT/NL regex, skip NER)
- Pros: single runtime, minimal ops, lowest latency
- Cons: no NER ever → person name / address / DOB in free prose missed → loses parity with Presidio-based competitors (Nightfall, Harmonic); no tier-2 differentiation; EU multi-country sale still works for structured but fails for unstructured prose
- Rejected: unstructured detection is non-negotiable for Business tier; competitors already ship it
Alternative B — Python Presidio sidecar for everything, deprecate Go recognizers
- Pros: single detection layer, one language (Python), Presidio ecosystem, easier custom recognizer story
- Cons: throws away working, tested Go code; every classify call hits Python (bad p99); heavier ops; Python cold start not acceptable for Starter pricing; breaks air-gap story for low-end self-host
- Rejected: Go recognizers are a performance and cost moat we want to keep; Starter tier cannot carry a Python runtime at 8€/u margins
Alternative C — WASM-only NER for all languages (no Python at all)
- Pros: no Python process, everything runs in browser or edge; uniform deploy model
- Cons: 5× ~45 MB models to ship to every user is prohibitive; browser RAM budget blown; custom recognizer hot-reload impossible in extension context; tenant-level rules cannot live on client (trust boundary); spaCy-equivalent quality not reachable with distil-models alone
- Rejected: browser is not the right home for multi-lang NER or tenant-specific custom recognizers
Alternative D — Third-party API (AWS Comprehend, GCP DLP, Azure Cognitive Services)
- Pros: zero infra, immediate coverage
- Cons: sends customer data to non-EU cloud, kills sovereign pitch, per-call cost makes 15€/u unviable, vendor lock-in, no air-gap path
- Rejected: fundamentally incompatible with our positioning
Follow-up Actions
- DT-1: ship Go recognizers EU (structured)
- DT-2: ship CamemBERT Web Worker
- DT-3: create
noxys-presidiorepo, MVP - DT-4: integrate sidecar in noxys-api with PlanGate + circuit breaker
- DT-5: deploy manifests
- DT-6: tier gating + Stripe
References
- prd-detection-tiering
- detection-multitier-architecture
- nlp-strategy
- ml-benchmark-plan
- onnx-pipeline