The Double Blind — verifiable privacy
Witbitz's core privacy claim: for an app built on it, neither the platform operator nor the app's own owner can read the users' data. And — the part that makes it a property not a promise — that claim is meant to be checkable from the code, not taken on faith.
This doc is the whole argument in one place: what holds today, exactly where it stops, and Sealed Spaces (§5) — the opt-in tier that turns the remaining gap from a behavioural promise into a property of the build.
See also: async Spaces (the sealed ledger + decrypt-once render), the Space link is the auth (the keys live in the link), and Verify it yourself (run the checks).
1. Ciphertext at rest → the platform is blind
A Space's data is a snapshot sealed under the room key mk and stored as ciphertext; the platform never holds
mk (agent/envelope.mjs seal/open; the key is committed only as commit(mk), room-link-auth).
At rest, to the operator, a Space is opaque bytes — storage and transport are content-blind.
There is no operator recovery key. Every record is sealed to the room key alone — the platform writes no support or recovery recipient of any kind, so there is nothing readable-by-the-operator to read at rest. The double blind is the only configuration that runs in production.
2. The decrypt-once render → the one plaintext door
Plaintext opens in exactly one code path — the certified render — but that path runs on two triggers: an
AI turn (open the ledger with a present member's mk, run the declared program, re-seal, drop mk) and
every changed read (the poll decrypts the new entries to return them, because the client is thin and decrypts
nothing itself — async Spaces §3–4). Reads outnumber turns, so the honest exposure surface is
the render, not "only when the AI acts." The key reaches that render only from a present member's client, per
active window — never from storage — so the operator can act on the data only while a member is present, and learns
nothing at rest. Keeping it one auditable path is exactly what makes the attested tier tractable.
The honest boundary, stated plainly. Because the client is thin, mk travels to the render on every
read and write, and the server opens the ledger in place — a decryption oracle for the moments a member
is active. So content-blind means at rest and against a passive (honest-but-curious) operator, not one
that captures mk in flight; and because the whole ledger is sealed under one long-lived room key, an
operator that logged mk once could read that Space's history past and future — there is no per-Space
forward secrecy yet.
That boundary is the reason §5 exists.
3. Backendless → the app owner is blind too
The second blind is the one nobody else offers. A Witbitz app is a static page with no backend of its own that connects only to the audited Witbitz API. So the app's developer/owner has no backend through which to read its users either: the users' content is sealed to a key the owner doesn't hold, and the app has nowhere else to send it. Data is encrypted from the owner, not merely to them.
To be exact — "backendless" is a claim about the app, not the platform. Witbitz is still a backend (one shared, constrained, inspectable platform). What's removed is each developer's own, opaque, app-specific server. So the trust surface isn't smaller in a hand-wavy "no infrastructure" sense — it's consolidated: one auditable platform to check, instead of N unknown app backends. That consolidation is exactly what makes the footprint declarable (§4).
4. Verifiable → check the code, not the operator
Because the app is a static page with a declared egress (it can only talk to the Witbitz API), its entire data footprint is inspectable and declarable. You verify the code and a signature, not a "trust us" policy.
Four of those checks run in your own terminal today — the admission gate enforcing, the egress lock, the content-blind store, and the deployed render being a reproducible build of published source. Verify it yourself is the commands. What is not yet checkable about your own traffic is the render being unobservable in use; see §6.
5. Sealed Spaces — the tier that removes the key from our reach
Opt-in, and a tier — not a replacement.
Today's Spaces are double-blind, but §2's blindness is behavioural: the operator is trusted not to look at a key
it does, briefly, receive. A single capture defeats it — a logging toggle, a compelled order, a compromised render
reads the entire history, past and future, because the key arrives on a schedule and nothing bounds one room's
exposure. A Sealed Space takes the key off that schedule. The server never receives mk. There is no capture to
toggle. The platform stores ciphertext and holds exactly one capability — the ability to append, never to
read.
5.1 The design — four changes, each assembling a primitive already in the platform
- The ledger becomes an append-only log of individually sealed entries — not one snapshot re-sealed on every write. This is the load-bearing move: appending no longer requires reading, which is exactly what today fuses "write" to "hold the key."
- The client decrypts locally. It syncs sealed history once, caches it on the device, then pulls sealed deltas by cursor — the same poll shape as today, just opaque to the server.
- A session-scoped, ephemeral actor does the reasoning. On the first turn the client seals the assembled
context to the actor's session public key; after that the actor receives only new messages. It never sees
mk, holds nothing across sessions, and is evicted on idle. - Write-back is a write-only capability. The actor appends its reply by sealing to the Space's public write
key (
sealTo/openBox). It can add to the ledger without being able to open it.
The net: the platform routes ciphertext, stores ciphertext, and can append ciphertext. It cannot read the room.
5.2 What it protects — and what it doesn't
A security claim is only worth the threat model it names. Three columns, not two.
Structurally protected: at-rest capture (storage dumps, a compelled disk or database order, a logging toggle, a compromised render — the platform holds only ciphertext and a write-only key); and blast radius, because per-Space epoch keys mean one Space's compromise doesn't cascade and rotation bounds it going forward.
Not protected, stated plainly:
- The live session is plaintext to the ephemeral actor while it reasons. Sealed Spaces closes the at-rest hole; it does not make the AI compute over data it cannot read. That is the attested tier.
- The endpoints. A compromised member device sees plaintext; true of every end-to-end system.
- Metadata — who is in a Space, when they post, how large the messages are. Sealing content doesn't seal traffic shape.
Naming the third column is not a weakness. It is the honest meaning of content-blind: blind at rest, bounded in use.
5.3 What it costs
- No member-absent turns. With no client present, nothing decrypts the context to feed the actor — so "the agent acts while everyone's away" (delegated authority) doesn't exist in a Sealed Space. This is why Sealed is a choice, not the default.
- Prompt caching, likely lost. An ephemeral per-session actor re-receives the full context each session, so server-side prefix caching probably goes away. Likely the largest recurring cost; worth measuring before committing. A warm-actor variant — residency billed, context held longer — is the dial that trades a little blindness back for cache.
- Cold-device sync. A new device pulls sealed history before the first render — a first-open cost, not per-turn.
- Actor residency is billed while a session is open; not scale-to-zero for the length of a burst.
- Forward-only revocation. Removing a member is an epoch re-key — the mechanism the Bridge already runs. It cannot un-share what they already read; inherent to end-to-end, and worth saying out loud.
5.4 Status
In hand: per-entry sealed append and content-blind polling (scaffolded behind a flag, inert until enabled);
sealTo/openBox for write-only append; epoch re-key in the Bridge; the reproducible-build + egress-lock +
certificate stack. To build: the sealed client → actor session channel; ephemeral-actor lifecycle and idle
eviction; cursor-based sealed-delta sync as the default read path; epoch-key distribution to members without the
platform in the loop. To measure first: the real prompt-caching cost on a busy Space.
6. Status — what holds today
Being precise, because this is exactly where a claim can overreach:
| Property | Status |
|---|---|
| Ciphertext at rest (sealed ledger, platform holds no key) | shipped |
| No operator recovery key (sealed to the room key alone) | shipped & CHECKABLE — POST {"op":"sealed"} → recipients:["room"] (verify §Test 5) |
| Admission gate enforces (reads and writes refused without an allow-listed identity) | shipped & CHECKABLE (verify §Tests 1–3) |
Decrypt-once render, mk from the client only, dropped after |
shipped |
| Backendless static app (no backend of its own) | shipped |
Egress-lock (CSP connect-src/script-src allowlist) |
shipped & ENFORCED (block-mode) (verify §Test 4) |
| Client E2EE primitive (the sealed-blob envelope) | shipped |
| Signed build certificate (deployed build hash + egress, signed from ground truth) | shipped & CHECKABLE — /cert.json (verify §Test 6) |
| Reproducible build of published source (rebuild it → the deployed hash) | shipped & CHECKABLE — /source.tar.gz (verify §Test 7) |
Sealed Spaces (the platform never receives mk) — §5 |
designed — primitives in hand, the session channel is not built |
| Attested tier (operator provably blind in use) | built & independently verifiable — NOT on production traffic (the attested tier) |
| Attested client (knowing the code running on the user's side) | design (the attested client) |
7. The moat — honestly
The moat is not "incumbents can't do this." They can: Apple's Private Cloud Compute ships attested confidential inference today, and a competent team could match everything Witbitz has shipped in a quarter. Two things are actually defensible:
- Neutral + backendless. A vendor cloud reads to itself by default; a Witbitz app has no backend of its own and isn't bound to one provider's stack. The trust surface is consolidated to one auditable platform, not removed — the honest claim, not "no backend" (Witbitz is the backend, singular and inspectable). Awkward for an incumbent whose business is the readable backend to adopt — but not something they're barred from.
- The verifiability layer. The signed certificate and the reproducible build have shipped, and the attested tier is built and independently verifiable — though not yet serving production traffic. So the honest position is no longer "a direction we're building toward": four checks run in a stranger's terminal today, and the fifth is demonstrated on hardware and waiting on operations rather than on invention. What remains genuinely unbuilt is Sealed Spaces (§5) and the attested client.
8. Code map
| Concern | Where |
|---|---|
At-rest ledger seal (symmetric, under mk only) + key commitment |
agent/envelope.mjs (seal, open, commit, newRoomKey); agent/sessionStore.mjs encodeMemory — one recipient, mk. No operator/beta/admin recipient in prod. |
Recipient sealing (ECDH sealTo/openBox) — seals to a member/agent public key, never an operator |
scoped agent reads (agent/spaceScopedView.mjs), Bridge epoch keys (agent/bridgeMembership.mjs), the collection-blob — a separate mechanism from the at-rest ledger above |
| Sealed ledger + decrypt-once render | agent/sessionStore.mjs, agent/asyncTurn.mjs |
| Content-blind poll (etag over ciphertext) | agent/spacePoll.mjs |
| Egress-lock CSP (block-mode, enforced on prod Spaces) | spaces/tools/gen-space-csp.mjs |
| The render, published and readable (~600 lines) | github.com/kibitz-chat/witbitz-render |
Sealed Spaces is not "trust us less." It is "you don't have to." The current tier remains for the workflows that need an always-on agent; Sealed is for the rooms where the key should never leave your hands — and where you can prove it didn't.
Machine-readable source: the-double-blind.md · every doc in one fetch: llms-full.txt · ← async-spaces · the-live-call →