backchannel

Put AI in the meeting. Keep the names out of the model.

Backchannel is an open-source meeting assistant you run yourself. With its PII Shield on, a client's name, company, email address or phone number becomes a per-session token the moment it is written, and every model, on this machine or in a cloud, reads the token. This page is the long version of that claim: the mechanism, the parts that are enforced rather than advised, the limits, and the file to open when you want to check it yourself.

Off by default · detection runs on this machine · MIT licensed · free desktop downloads, no sign-in needed

The promise

What a model receives, and what you do

Paste a sentence into the Privacy tab and it shows the sentence a model would receive, with a legend for every token and the layer that found it. Nothing typed there is stored. The same substitution runs on every transcript line, directive, document excerpt, session name and speaker name in a real session.

The Privacy tab's try-a-sentence box. Typed in: Owen Delacroix from Alderwake Health Network owns the identity approval, reach him at owen.delacroix@alderwake.example or 212-555-0142 before the board review. Shown as a model would receive it: [PERSON_1] from [ORG_1] owns the identity approval, reach him at [EMAIL_1] or [PHONE_1] before the board review. A legend lists each token's value and whether the on-device name model (ner) or a pattern found it.
FIG. 1One sentence in, the sentence a model receives out. Two tokens came from the on-device name model, two from patterns. The email and phone number were never going to be missed; the name and the company are the harder catch.

Nine categories, eight on by default

People, organizations, places, email addresses, phone numbers, national identifiers, payment-card numbers, IP addresses and street addresses. Card numbers must pass a Luhn check, so a long invoice reference is not mistaken for a card. Places are off by default, because “we are expanding into Texas” is analysis, not identity; turn the category on if a city is sensitive in your work.

Numbered per session, unlinked across sessions

Tokens read [CATEGORY_n]. Owen is [PERSON_1] on every line of one call, so the analyst can follow who promised what to whom. In the next session he is nobody in particular. The vault stores a keyed hash of each value rather than the value, so nothing in the database links a person from one session to another.

The shield in ordinary use

A protected session says so in its header: a count of shielded values beside the export button. The outcome line, the commitments and the open loops on this screen were all produced by models that never saw the names they are shown with. The names were put back on the way to the screen, and the badge is the only sign that anything happened.

A completed session's Overview for Alderwake Health Network's recovery readiness review, 46 minutes across two calls. The session header carries a 9 shielded badge beside the Export button. Below: the top outcome, Sponsor aligned on a non-disruptive pilot, then counts of 4 commitments, 3 open loops, 3 opportunities and 3 risks, and lists of commitments naming Owen and Leah.
FIG. 2A shielded session after the call. The 9 shielded badge counts the values in this session's vault; Owen and Leah appear here because this is the screen, and nowhere else.
The mechanism

Tokenized at the door, not filtered at the exit

The usual design for this feature is an outbound filter. Text is stored as spoken, and something scrubs it just before the model call. A reviewer will recognise the failure mode: every new prompt builder has to remember to call the filter, and the one that forgets is the leak.

Backchannel encodes at ingress instead. Every path that writes human text passes through protect_text (or protect_name, for speakers) in backend/app/services/pii/shield.py before the row exists. The analyst, the objection handler, the synthesizer, strategic signals, the briefing, chat and Ask all read stored text, and stored text is already tokens. No prompt builder was changed to add the shield, and none can bypass it, because there is nothing to bypass. The plain value never reached the database.

Every write path, and the file where the encode call sits. Paths are under backend/app/.

Text arrives asTokenized in
Live transcript segments, as each diarized segment is transcribed ws/audio_handler.py
Transcript imports (.txt, .md, .docx), audio imports and re-transcription routers/imports.py
Manual transcript entries routers/transcripts.py
Directives, typed before the call over REST or mid-call over the WebSocket routers/directives.py, ws/audio_messages.py
Session name, notes and meeting context routers/sessions.py
Speaker names, which are a person's name by definition routers/speakers.py via protect_name
The document excerpt, extracted on this machine routers/documents.py
The question you type into Ask during a call routers/ask.py
Your turns in Chat routers/chat.py
Detection

Found on this machine, in four layers

No layer makes a network call. Each catches what the one before it cannot, and the roster is what makes a name caught once stay caught.

Patterns, for the structured categories

Email, phone, national identifier, card, IP and street address are shapes, and shapes are what regular expressions are for (services/pii/recognizers.py). A card candidate must pass Luhn, so a thirteen-to-nineteen-digit reference number stays a reference number.

The roster, matched as whole words

The session's speakers, the protected-terms list you maintain for the workspace, and every person, organization and place already in the session's vault. A name caught once by any layer is caught on every later line, with or without the model. Each capitalized part of a multi-word name maps to the same token, so a later “Delacroix” on its own still becomes [PERSON_1]. The protected-terms list is where you put what a model would miss: client companies, project code names, a person who is never introduced.

Introductions

“My name is” and “this is” mark the name that follows as a person, which is how most people enter a call.

An on-device name model, optional

Xenova/bert-base-NER, the CoNLL-2003 BERT model as a quantized ONNX file of about 110 MB. It downloads once into DATA_DIR/pii-models/ and runs on the onnxruntime the app already ships for speaker diarization, on the CPU. If it cannot be fetched, the shield keeps working on the other three layers and the Privacy tab says so rather than showing a green light.

When layers disagree, the roster wins, then patterns, then introductions, then the model. The roster knows that Brown is Bill Brown and that Cyberdyne is a company; the model only guesses a category.

The Privacy tab in full: category checkboxes with Places unticked and the others on; an On-device name recognition block marked Ready with the model named as Xenova/bert-base-NER, about 110 MB; a Protected terms field with an Organization type selector and Add button; and the Try a sentence box with its tokenized result and legend below it.
FIG. 3Everything you control, on one tab: which categories are on, whether the on-device model is used and whether it is ready, the protected terms you add, and a scratch box to test any of it.
Storage and reveal

Encrypted at rest, decoded only at the edge

The vault (services/pii/vault.py, table pii_vault_entries) holds each value Fernet-encrypted under a key derived with HKDF from the credentials master key, the same root of trust that protects your provider API keys, through secrets.derive_subkey. Lookup uses a keyed HMAC of the normalized value. So the table reveals neither the values nor whether two sessions share one; the same person in two sessions is two unrelated rows.

Decoding is reachable from exactly three places, all of them on the way to the person at this machine:

  • PiiRevealMiddleware substitutes tokens in every session-scoped JSON response and in the session list.
  • RevealingWebSocket does the same for every live message during a call.
  • The exports and /api/chat, whose session is not in the URL path, reveal explicitly through shield.reveal_text.

Nothing under services/agents and nothing in services/llm.py imports the reveal path. That is an invariant you can enforce with a grep, and the verification section below gives you the command.

Your screen is the edge

During this call the transcript shows Owen and Maya, the answered question quotes Owen's threshold, and the ask bar sends your questions to a Gemini model. Every one of those model calls carried [PERSON_1]. The names on screen were put back by the WebSocket wrapper on the last hop, and each substitution was counted.

Backchannel during a live call: three strategic signals across the top, 125 live insights with a question you asked already answered, and a live transcription column on the right where Owen, Me and Maya are named on each line. The ask bar along the bottom shows a Gemini model selected.
FIG. 4A live call with the names on screen. The transcript column, the answered question and the signal cards all reached the browser through the revealing WebSocket; the models that produced them saw tokens.

Exports and the audit trail

Exports carry tokens unless you ask otherwise. The Export menu has a box, Include personal data, which passes ?reveal=1; leave it unticked and the transcript, the insights workbook and the summary all leave with tokens in place. Every reveal, on screen or into a file, appends a row to pii_reveal_events with the session, the route and the number of tokens replaced, and the Privacy tab shows the last 24 hours.

Audio

Audio is enforced, not advised

Text can be tokenized before a model reads it. Audio cannot, so the rule for audio is a lock rather than a setting.

Locked to local models while the shield is on

While the shield is on, transcription_runtime.audio_lock_reason locks audio to local models the way Privacy First does, but for audio alone. A cloud batch transcriber is coerced to a local model; a cloud live-caption gateway is paused, and the live call says “Live captions off: PII Shield” rather than going quiet. Cloud text models stay available throughout, because text is tokens by the time they see it. Uploaded documents are read on this machine and never sent as files. The Privacy tab reports each row as it stands, including a cloud gateway that is configured but paused.

Admin Privacy tab with the PII Shield on, badged Personal data tokenized. Four coverage rows: transcripts, insights, briefings, chat and documents protected; transcription audio protected, held to a local model; live captions protected; transcript refinement not covered because the refiner is off. Below them: Vault, 9 protected values across all sessions.
FIG. 5The coverage report with the shield on. Three rows are protected by enforcement; the fourth says “not covered” because the optional refiner is off, which is the honest answer rather than a fourth green tick.

Closing the quality gap: the Transcript Refiner

Local transcription is rougher than cloud transcription: thin punctuation, flat casing, a product name split in two. The optional Transcript Refiner agent, off by default, sends the tokenized transcript to any text model, local or cloud, to fix punctuation, casing, sentence boundaries and obvious mishearings. It runs every 45 seconds during the call, once more at call end, and before post-import analysis. A rewrite is kept only if it carries exactly the original tokens; a model that drops, invents or renumbers one loses that entry, and the transcriber's text stands.

Verification

Check it, instead of taking our word

Three checks, in increasing order of effort. None of them needs a network.

Read the prompts as they left

Turn on Record outbound prompts in the Privacy tab and every prompt is appended to DATA_DIR/prompt-log/outbound.jsonl exactly as it was sent, with the agent, the model and the session. The tab lists the newest entries with a badge, “tokens only” or “blocked”. The file is written raw on purpose, bypassing the log scrubber, because a scrubbed record could not show a leak. It never leaves the machine, and one button deletes it.

The tripwire

Independently of the log, while the shield is on every text prompt passes an egress check (services/pii/egress.py, called from generate_text and generate_json). A prompt that still carries a value the vault has seen in plaintext is refused before it is sent, and the refusal is written to the audit trail. A gap upstream costs one model call, not one disclosure.

The grep

The decode path must not be reachable from any prompt builder. From a checkout of the repository:

# Expected output: nothing.
grep -rl "reveal_" backend/app/services/agents backend/app/services/llm.py

# The three places that do decode, all on the way to the screen:
#   backend/app/services/pii/reveal_middleware.py   session-scoped JSON and the session list
#   backend/app/services/pii/ws.py                  every live WebSocket message
#   backend/app/routers/artifacts.py, chat.py       exports and /api/chat
Limits

What the shield does not do

A privacy page that only makes promises is not worth sending to a reviewer. These are the edges, in the product's own words.

It is off by default

Turn it on in Admin, Privacy. A fresh install protects nothing until you do.

Detection is very good, not perfect

An unusual name that is never introduced and is on no roster can be missed. That is exactly what the protected-terms list is for: add the names a model would not know.

Places are off by default, on purpose

Place names are usually analysis rather than identity. Turn the category on if a city or a site is sensitive in your work.

Earlier sessions keep what they hold

Sessions recorded before the shield was on stay as they were until you protect them. POST /api/sessions/{id}/pii/protect runs the encode path over a stored session, and the product offers it.

Tokenization is reversible by design

This is not redaction. The vault exists so your own screen can show the real names. Anyone with the data directory and the master key can decode, so protecting the master key still matters.

It governs what models read

It is not a claim about your disk, your backups, or who can log into the machine. Those are yours to secure, as with any software you run.

Two switches

Privacy First decides where. The shield decides what.

Different switches, and the difference matters

Privacy First keeps everything on this machine and turns off anything that needs an outside call. The PII Shield decides what any model, local or cloud, gets to read. They compose. A configuration many teams will land on is shield on with cloud text models: frontier-model analysis quality, and no name ever leaves. A fully local team turns both on and loses nothing the shield offers.

The Admin Agents tab. At the top, the Privacy First switch, currently off and badged Cloud AI allowed, with its description: keeps every byte of call audio and transcript on this machine. Below it the agent lineup begins with the Audio Bridge and the Consolidated Analyst, each with its own model selector and enable switch.
FIG. 6Privacy First sits above the agent lineup on the Agents tab. The PII Shield lives on the Privacy tab beside it. One decides where models run; the other decides what they may read.
QuestionPrivacy FirstPII Shield
What it decides Where processing happens What any model is allowed to read
Cloud text models Turned off Allowed; they receive tokens only
Transcription audio Local models only Local models only, enforced while the shield is on
Live captions On-device captioner only Cloud gateway paused; on-device captioner allowed
Uploaded documents Read on this machine Read on this machine, never sent as files
Default Off Off

Questions a reviewer asks

Can I use cloud models such as Gemini or GPT with the shield on?

Yes, for text. Every agent, the briefing, chat and Ask send tokenized text, so a cloud text model receives [PERSON_1] and never the name. Audio is different: while the shield is on, transcription is held to a local model and a cloud live-caption gateway is paused, because audio cannot be tokenized.

Does any detection step call out to a service?

No. Pattern matching, roster matching, introduction patterns and the optional on-device name model all run on this machine. The model is an ONNX file of about 110 MB downloaded once into DATA_DIR/pii-models/; if it cannot be fetched, the other layers keep working and the Privacy tab says so.

Is this redaction? Can the names be recovered?

It is tokenization, which is reversible by design. The real values sit in a vault encrypted under a key derived from the credentials master key, and they are put back only on the way to your screen, or into an export when you tick Include personal data. Anyone with the data directory and the master key can decode, so protect the master key.

What happens to sessions recorded before I turned it on?

They keep their plain text until you protect them. POST /api/sessions/{id}/pii/protect runs the encode path over a stored session's transcript, insights, directives, document excerpts, session fields and speakers, and reports what changed. The product offers it for those sessions.

How do I verify that a prompt really left with tokens only?

Turn on Record outbound prompts. Every prompt is written to DATA_DIR/prompt-log/outbound.jsonl exactly as it was sent and badged tokens only or blocked in the Privacy tab. Separately, while the shield is on, a prompt still carrying a vault value is refused before it is sent.

What is the difference between Privacy First and the PII Shield?

Privacy First decides where processing happens: it keeps everything on this machine and turns off anything that needs an outside call. The PII Shield decides what any model, local or cloud, gets to read. They compose. Shield on with cloud text models gives frontier-model analysis with no name leaving the machine.

Try it on a sentence before you try it on a client

Install, open Admin, Privacy, and paste the worst sentence you can think of into the try-a-sentence box. Nothing typed there is stored, and the legend tells you which layer caught what.