Skip to content
Logixlysia
Esc
navigateopen⌘Jpreview
On this page

Neural PII redaction

Mask names, addresses and IDs with an on-device model before logs leave the process

autoRedact is a synchronous pattern pass: it masks emails, IP addresses, Luhn-valid card numbers, JWTs, and any value under a sensitive key name. What it cannot see is free-text PII — a person’s name, a street address, a phone number, a national ID — because those have no fixed shape.

A token-classification model does see them. The logixlysia/desertant subpath wires one in at the transport boundary, so console output keeps its current speed and only records leaving the process pay for inference.

import { Redact } from '@desert-ant-labs/redact/native'
import { Elysia } from 'elysia'
import logixlysia from 'logixlysia'
import { createAxiomTransport } from 'logixlysia/axiom'
import { withRedaction } from 'logixlysia/desertant'

const app = new Elysia().use(
  logixlysia({
    config: {
      autoRedact: true, // fast pattern pass, every record
      transports: [
        withRedaction(createAxiomTransport(), Redact.load, {
          onError: error => console.error('[redact]', error)
        })
      ]
    }
  })
)
GET /orders/42 200 — Anna Müller, Hauptstraße 5, +49 151 23456789
   console  →  Anna Müller, Hauptstraße 5, +49 151 23456789
   Axiom    →  [GIVEN_NAME_1] [SURNAME_1], [STREET_NAME_1] [BUILDING_NUMBER_1], [PHONE_1]

No dependency is added

Logixlysia does not import @desert-ant-labs/redact — you pass the model in, and the subpath only describes the shape it needs:

interface NeuralRedactor {
  redaction: (
    text: string,
    options?: NeuralCallOptions
  ) => Promise<{ redactedText: string }>
  withCallGroup?: <T>(body: (group: string) => Promise<T>) => Promise<T>
}

Anything matching that works — another model, a stub in tests, a call out to a DLP service. Nothing about Desert Ant’s install size (~195 MB of native and Wasm builds), its runtime model download, or its source-available licence (free below 100,000 monthly active devices per platform) reaches users who do not opt in.

The model itself is installed by you:

bun add @desert-ant-labs/redact          # server-side inference in Node.js

Its native build covers linux-x64, linux-arm64 and darwin-arm64; elsewhere load() throws a clear error.

Loading the model

withRedaction accepts a redactor, a promise for one, or a loader called once on the first record. Passing Redact.load directly keeps the model download off the boot path:

withRedaction(transport, Redact.load)                 // lazy, on first log
withRedaction(transport, await Redact.load())         // eager, at boot
withRedaction(transport, () => Redact.load({ directory: '/models/redact' })) // self-hosted

A failed load stays failed — retrying per record would hammer the model host — and every affected record is reported through onError.

Behaviour worth knowing

Redaction runs off the request path. log() returns immediately and the record is delivered when the model finishes, so a slow model never adds latency to a response.

Order is preserved. Records reach the transport in the order they were logged, not in the order inference happens to finish.

It fails closed. A record the model could not process is dropped and reported, never forwarded unredacted. Set onFailure: 'forward' only when losing the log is worse than shipping raw PII.

The queue is bounded. Inference is far slower than logging, so at most maxQueue records (default 1000) wait at once; beyond that, records are dropped and reported rather than growing the queue until the process runs out of memory. A record dropped this way is never forwarded, whatever onFailure says.

Errors are rebuilt, not mutated. An Error in meta gets a redacted copy — message, stack and own fields — while the original instance is left untouched for the rest of the pipeline.

Options

Option Type Default Description
includeMeta boolean true Also walk meta and redact the strings inside it
labels Iterable<string> model default Restrict redaction to these PII categories
maxDepth number 4 Nested levels of meta walked; deeper values pass through
maxQueue number 1000 Records allowed to await the model before new ones are dropped
minimumConfidence number model default Neural confidence threshold; deterministic recognizers always apply
onError (error: unknown) => void Called on load failure, redaction failure, and dropped records
onFailure 'drop' | 'forward' 'drop' What to do with a record the model could not process

The returned transport exposes flush(), which awaits the queued records and then flushes the wrapped transport — call it before process exit.

Restricting what gets masked

Every string costs one model call, so narrowing the categories is the cheapest lever when throughput matters:

withRedaction(transport, Redact.load, {
  labels: ['GIVEN_NAME', 'SURNAME', 'STREET_NAME', 'PHONE'],
  includeMeta: false // message only
})

Keep autoRedact: true on as well: the pattern pass protects console and file output, which never reach the model, and it costs microseconds.

Last updated on September 12, 2026

Was this page helpful?