Witbitz docs HomeTrustAll docs

All documentation

Overview — build static AI apps around Spaces

Witbitz is a trusted runtime for collaborative AI applications. You build the app as a static website; Witbitz provides the shared room, identity, permissions, agents, storage, payments, and execution underneath it.

The important primitive is a Space: a persistent room where people, an application, and one or more AI agents work together under explicit permissions. A Space can be a product by itself, or it can sit inside another app as the place where users ask, approve, and inspect what the app does.

Start here if you are new. For the complete architecture argument, see the platform. For the current shipped/preview/designed map, see status.


The short version

Your app does not need its own private backend for the collaborative surface. It can be a static page that talks only to Witbitz:

  1. The page creates or opens a Space.
  2. Members join by link, sign-in, invite, or owner policy.
  3. The Space stores conversation, documents, artifacts, and shared widget state.
  4. An AI agent runs when a member posts a turn.
  5. High-stakes tool calls become proposals; an authorized human approves before anything executes.
  6. The data footprint is declared and checkable: sealed storage, locked egress, signed build metadata, and a reproducible render.

That is the trade: replace many app-specific backends with one constrained runtime whose behavior can be inspected.

Static appLoads in the browser with a locked egress footprint.
SpaceHolds members, admission policy, sealed config, and shared state.
Agent turnRuns only when a member acts, then re-seals and goes cold.
EvidenceDocs, OpenAPI, CSP, build certificate, and raw sealed bytes stay inspectable.

What a Space gives you

Primitive What it means
Room identity A Space carries its own identity, members, keys, and admission policy.
Shared state Everyone sees the same thread and the same live widget state.
Agent turns The AI is invoked as part of the room, not as an external chatbot bolted onto the side.
Tools and widgets App functions become tools; tool results can render as maps, charts, files, or custom widgets.
Delegated authority The agent can propose an effectful action, but the runtime requires human approval before execution.
Content-blind storage The store holds ciphertext sealed to the room, not to the platform operator.
Verifiable footprint The app is static and egress-locked, so the hosts it can reach are visible and enforced.

The first app: Spaces

The first product built on the runtime is Witbitz Spaces: a multiplayer ChatGPT-style room where several people and an AI collaborate over time. It can read shared documents, search, build maps, render charts, generate artifacts, and keep the room alive between visits.

Spaces matters because it is not a special internal product. It is the reference app for the platform surface: collections, artifacts, agent tools, widgets, identity, and the content-blind render.

The two guarantees

Witbitz docs are organized around two guarantees:

  1. Humans keep authority. Agents do not own credentials. They propose; people approve; the runtime executes only approved actions.
  2. Privacy is verifiable. The app has no private backend of its own, storage is sealed to a key we never hold, egress is locked, and the render's deployed build can be checked against published source.

The second guarantee has important status boundaries. The production tier is content-blind at rest, with plaintext opening briefly in the server-side render while a member is active. The attested server tier — a Space whose turns run inside a measured Nitro enclave, with your key sealed to that image — is now in production; it launches on demand and scales to zero when idle. Enabling it is the app builder's per-room choice — the platform can run any room in the enclave — and the model call leaves the measured image only through an attested no-log gateway into confidential-compute inference on an open-weights model — the frontier-vendor residual applies only to rooms that choose the frontier trade. Those distinctions are deliberate and should stay visible in every security conversation.

The three layers

Layer Role
Interaction How people and AI meet: async Spaces today, live calls through the real-time engine, and future modes.
Platform The runtime services: identity, permissions, agents, sealed storage, payments, secure execution, API.
App A static page with no backend of its own, connecting only to Witbitz.

The app is not "backendless" in the absolute sense. Witbitz is the backend. The point is that the backend is shared, constrained, and inspectable instead of being an opaque server unique to every app.

Where to go next


What Witbitz Is — the platform

Witbitz is a trusted runtime for building collaborative applications where humans, applications, and autonomous AI agents work together safely, under explicit permissions. You build your app as a static website with a verifiable privacy policy, and Witbitz provides the runtime underneath. Read this first; the other docs go deep on each piece.

One line: a runtime for people and their AI agents to collaborate, where privacy is verifiable and humans keep the final authority.


1. The idea

The internet was built for users and applications. The next generation needs a runtime where people, apps, and autonomous AI agents collaborate — but safely, with permissions that are explicit and revocable, and with a human always able to say no. Today's AI ecosystem is siloing: every vendor wants your users, agents, memory, and work inside its own platform, on a backend it can read. Witbitz is the sovereign alternative — your Space holds your people, agents, and data, and nothing leaves it unless you allow it.

2. What the runtime provides

A collaborative app needs the same primitives over and over; Witbitz provides them so the app can be a thin static page:

  • identity — who a participant is (see the link is the auth)
  • permissions — who may do what (per-member capabilities; delegated, revocable authority)
  • agents — hosting + running the AI that lives inside the app
  • storage — the sealed, content-blind ledger + documents
  • payments — metering + wallets for agent compute
  • secure execution — the decrypt-once render where the AI acts and changed reads are served, then drops the key

3. The three layers

Witbitz is three layers — this is its architecture, not a product stack:

  1. Interaction — how humans and AI meet. Today: live calls and async Spaces (more modes to come).
  2. The platform — a general-purpose app backend: the whole app flow, plus the AI that runs inside the app. Exposed as the tenant-keyed /v1 API (API reference).
  3. The app — a static page with no backend of its own, connecting only to Witbitz. To be precise: this is not "no backend" — Witbitz is the backend (layer 2: identity, storage, agents, execution, permissions, billing). The proposition is narrower and more honest: you trade an arbitrary, app-specific backend for a single, constrained, inspectable platform one. That's what makes an app's privacy verifiable — there is exactly one trust surface and it is auditable, not that centralized infrastructure or trust dependency went away.

Everything happens in a Space: a self-authenticated room whose identity, members, and keys live in the link, not a central account server (room-link-auth).

4. The two guarantees (why it's safe)

  • Verifiable privacy — the double blind. A Space's data is sealed under a key the platform never holds. As long as the compute runs the declared program, the operator is blind to the data and activity — and so is the app's own owner. Neither can read the users' content. Because the app is a backendless static page talking only to the audited Witbitz API, its whole data footprint is declared and its privacy is checkable, not promised. (the double blind).
  • Human authority. Agents never own authority — they receive it. An agent proposes an action, the runtime evaluates policy, an authorized human approves or edits, then it executes — explicit, limited, revocable, audited. (delegated authority).

5. Built on a proven real-time engine

Witbitz's live call runs on an open, proven real-time engine — a pinned dependency — while the async Space is Witbitz's own. Stated precisely so nothing is overclaimed:

  • The live call is that engine (rebranded). That path gets its full peer-to-peer gate — account-free rooms, the join gate, cert-bound OIDC, end-to-end-encrypted media — by running the engine.
  • The async Space is Witbitz's own. It is a turn-based, content-blind HTTP service (no WebRTC, no OIDC, no cert-binding). The only thing it borrows is the invite-token wire format, reimplemented byte-compatibly for per-member identity (identity and admission).

For Witbitz's own layer (async Spaces, the platform API, the double blind, the Bridge), these docs are canonical.

6. The first app: Spaces

The first app built on Witbitz is Spaces — a multiplayer ChatGPT: a shared room where people and an AI collaborate together, with tools that render as live widgets and a room that persists. Try it: witbitz-spaces.pages.dev.

7. What you can build — the topologies

Spaces connect in different shapes; the valuable apps are the ones where the shape is the product:

  1. One governed room — an org's AI serves a person (tools + governance; the org can't read individual sessions). Customer service, a bank advisor, telehealth intake.
  2. Pro + AI with a private back room — both serve a client, and confer privately. Teacher + AI, doctor + scribe, lawyer + paralegal.
  3. Two sovereign sides — the Bridge — each side preps privately, they meet in a shared room. Negotiation, M&A, procurement, mediation.
  4. Hub & spoke — a person (or case) at the center, several orgs bridge in. Care coordination, eldercare.
  5. Many players — the Spaces app. Group planning, a family council, community rooms.
  6. A traveling personal agent — your Space + agent joins others with scoped permissions. Personal assistant, shopping concierge.

8. Access

The /v1 platform API is invite-only during private beta (tenant keys). The Bridge is an early protocol preview. Email hello@witbitz.chat.

9. Where to go next


Vault Spaces — the reference tenant app

Build a private, agent-backed Spaces app on Witbitz, as a tenant. Spaces — the flagship multiplayer AI room — is the reference app: you register a tenant, embed the reference Spaces, then deploy and verify it. Every step below is a real, exact operation you can run now.

Get Started path: this → Deploy itVerify it. Reference: Platform API (OpenAPI) · The SDK.


Quickstart — paste into an Ubuntu terminal

Want it running now? This registers a tenant and turns an agent config into a live Space hosted on Witbitz — no file to write, no server to run, no deploy. Needs only curl and python3 (both standard on Ubuntu).

bash
# 1) Register a free demo tenant — your builder identity on the platform (keys shown once; you'll need it for Deploy & attribution).
resp=$(curl -sX POST https://api.witbitz.chat/v1/tenants/demo \
  -H 'content-type: application/json' -d '{"name":"My first Space"}')
export WSK=$(echo "$resp" | python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["keys"]["secret"])')
echo "Tenant secret key (save it — shown once): $WSK"

# 2) Turn an agent config into a LIVE Space, hosted on Witbitz — then open the link it prints.
cfg=$(printf '%s' '{"agentName":"Helper","persona":"You help a small group plan and decide together."}' \
  | base64 | tr '+/' '-_' | tr -d '=\n')
echo "Your live Space: https://witbitz-spaces.pages.dev/embed-create#cfg=$cfg"

Open that URL and chat. It mints a fresh private room from your config and serves it — all on the Witbitz origin, with no page to host and no backend. The room is private by key-possession (the key rides the link fragment, never a server), and the config (name + persona) travels in the # fragment, so the server never sees it in the clear. Change the persona and re-run to reshape the agent.

This room is public — anyone with the link can open it (that's what "no secret in the flow" means). When you want rooms attributed to your tenant, two paths open up — both using the tenant you just registered:

  • Deploy on your own origin — a page you host embeds this same config from an origin you've declared.
  • A bespoke Witbitz-hosted app — Witbitz provisions your tenant its own full deployment: own origin, own key vault, own split-key ecosystem key, your config baked in, sign-in included (what you get).

Then verify it. The rest of this page explains each piece.


1. Register a tenant

You build as a tenant — your builder identity on the platform. Get a key pair with no invite using the demo path:

bash
curl -sX POST https://api.witbitz.chat/v1/tenants/demo \
  -H 'content-type: application/json' \
  -d '{"name":"My app"}' | python3 -m json.tool

Response (201, Cache-Control: no-store):

json
{ "data": {
  "tenant": { "tenant_id": "t_…", "status": "active", "roles": ["app"], "expires_at": "2026-09-24T…Z" },
  "keys":   { "secret": "wsk_…", "publishable": "wpk_…" }
} }
  • wsk_… — your secret key: server-side only, full scope, shown once — store it now (only its hash is kept).
  • wpk_…publishable: browser-safe, identifies your tenant; it does not authenticate an end user.
  • expires_at — a demo tenant is real and works immediately, but auto-purges at that time unless you keep it.

No coupon needed to follow this walkthrough — the demo tenant works as-is. It only auto-purges at expires_at; when you're ready to make it permanent (same id + keys), see Keep your tenant.

Calling the API. Base URL https://api.witbitz.chat/v1; present your secret key as Authorization: Bearer wsk_…. Success is enveloped { "data": … }, errors { "error": { "code", "message" } }. The OpenAPI is canonical — point tooling at https://api.witbitz.chat/v1/openapi.json (Platform API). Confirm your tenant:

bash
curl -s https://api.witbitz.chat/v1/tenant -H "Authorization: Bearer wsk_…" | python3 -m json.tool

2. Build the app — embed the reference Spaces

Your app is the reference Spaces, embedded. Two lines put the verified Spaces room on any page:

html
<script src="https://witbitz.chat/embed.js"></script>
<witbitz-space link="https://witbitz.chat/space#<your link>"></witbitz-space>

No link yet? Mint a room inline from a config — no pre-creation, no key handling on your page:

html
<div id="slot" style="height: 600px"></div>
<script src="https://witbitz.chat/embed.js"></script>
<script>
  // Your agent config, base64url-encoded into the link fragment (after #, so it never reaches the server).
  const config = {
    agentName: 'Helper',
    persona: 'You help a small group plan and decide together.',
  }
  const cfg = btoa(JSON.stringify(config))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '')

  const space = Witbitz.embed('#slot', {
    link: `https://witbitz-spaces.pages.dev/embed-create#cfg=${cfg}`,
    tools: {
      // A function on your page becomes a tool the agent can call.
      set_site_color: (args) => { document.body.style.background = args.color },
    },
  })

  // Turn metadata only — never message content.
  space.on('message', (msg) => console.log(msg.id, msg.self))
</script>

That last block is the pattern: a function on your page becomes a tool the agent can call. Witbitz.embed() also returns signIn, ask, invoke, setTheme, setState, and destroy — see embed.

Configure the agent — the same sealed config either way (the server never sees it in the clear):

Field Meaning
agentName Display name (≤ 80 chars)
persona System instructions (≤ 4000 chars)
tools Platform tools the agent may use — search_places, show_places, search_flights, … (tools & widgets)
immediateTools The subset that auto-runs; everything else becomes a human-approved proposal (delegated authority)
model Optional model override

How the tenant and the room fit. The embedded Space runs on the Witbitz origin and authenticates by key-possession — the room key rides the link fragment, never your page and never your tenant key. Your tenant is your identity on the platform (/v1): it meters usage to you and unlocks the platform surface (collections, sessions, agents). So the room is private by key-possession; the tenant is how the platform knows the app is yours.


Next


Deploy it

Your Spaces app is a static page — host it anywhere, on your own branded origin. The verified Space runs on the Witbitz origin inside the embedded iframe, so the room key and plaintext stay off your page.

Get Started path: Vault Spaces → this → Verify it.


Host the page

Host your HTML on Cloudflare Pages, Netlify, S3, or your own server — no build step, no backend. Two CSP notes for your page:

  • allow script-src for the origin you load from (https://witbitz.chat for embed.js, or the SDK origin);
  • for the headless SDK, allow connect-src for your Space endpoint (default https://api.witbitz.chat/v1/space).

That's it — the Space itself is served, verified, from the Witbitz origin inside the iframe.

If you host the Spaces app itself

If you host the Spaces app (rather than embedding the hosted one), embedding is off by default (frame-ancestors 'self'); set SPACE_EMBED_ORIGINS to the origins allowed to frame it (embed).

Production notes for your tenant

You registered a tenant in Vault Spaces — that meters usage to you and is your identity on the /v1 platform. When you go past the walkthrough:

  • Keep a demo tenant. A demo tenant is real and works immediately, but auto-purges at its expires_at. To make it permanent — same id and keys, no more expiry — upgrade it with an author coupon: bash curl -sX POST https://api.witbitz.chat/v1/tenant/upgrade \ -H "Authorization: Bearer wsk_…" -H "X-Author-Coupon: <code>" Or sign up non-expiring from the start — POST /v1/tenants/signup (X-Author-Coupon). No coupon? Email hello@witbitz.chat.
  • Owner-governed rooms — mandate admission tiers that are provably yours across every room your app creates: built, built and tested, not yet enabled in production (owner rule).
  • A bespoke Witbitz-hosted app — instead of hosting your own page, Witbitz provisions your tenant its own deployment: its own origin (witbitz-spaces-<yourapp>.pages.dev), its own key vault (a separate sign-in origin that confines the room keys — the same architecture the first-party Spaces runs on), its own split-key ecosystem key, and your agent config baked in. Sign-in needs zero setup on your side — Google and email-code run through the central Witbitz auth service, minted as your origin. Your app version is yours: it doesn't silently change when the reference Spaces updates. Rooms are stamped to your tenant and split-key by default. Provisioned per tenant — email hello@witbitz.chat with your tenant id and app name.
  • The billed /v1 surface is invite-gated private beta; the demo tenant and the keyless /space runtime work openly (Platform API).

Next


Verify It Yourself

Witbitz's claim is verifiable, not promised — so this page is where you check it, in your own terminal, instead of taking our word. It's the honest counterpart to every "enforcement-proven" line in the other docs: here are the exact commands, and here — just as plainly — is what they do not prove yet.

See also: The Verified Room (the model you're checking) · the Space link is the auth (how a real member passes) · the double blind (the privacy claim + its north-star).


What this page proves — and what it doesn't

Be exact about the scope, because "verify it yourself" is itself a claim that can overreach:

Status
The admission gate enforces — reads and writes are refused without an allow-listed identity, even when you hold the room key checkable now (Tests 1–3)
The app's egress is locked — the app you loaded can reach only a published allowlist of hosts; the browser blocks everything else checkable now (Test 4)
The store holds only ciphertext, sealed to a key the platform never holds (it can't read what it stores) checkable now (Test 5)
The deployed render is a faithful build of published source — rebuild it and get the same hash AWS runs checkable now (Tests 6–7)
The render is unobservable in use (operator can't read the plaintext during the decrypt instant) 🔶 Verifiable once the enclave is on — the tier itself is Production; turn it on and this becomes checkable on your own traffic (the attested tier); enabling it is the app builder's per-room choice

So today you can verify that the gate enforces — the load-bearing "who may read/write" layer — and that the front-end's data footprint is locked to a published allowlist (Test 4). You can also verify the store holds only ciphertext sealed to a key the platform never holds (Test 5) and that the deployed render is a faithful, reproducible build of published source (Tests 6–7). The one thing you cannot yet verify about your own traffic is that the render is unobservable in use — that during the transient decrypt-and-render instant the operator can't read the plaintext out of memory; that needs the attested/TEE tier (the double blind §4–6). That tier is now in production, and independently verifiable — the room key is generated inside AWS Nitro hardware, the image measurement is reproducible from published source, the whole check runs in your browser (the attested tier) — turn on "Run this Space inside the enclave" and its key is sealed to a key born inside the measured image, verified in your browser against the running enclave's published measurement (/enclave-claim.json) with a fresh nonce. Enabling it is the app builder's per-room choice — the platform can run any room in the enclave — and the model call leaves the measured image only through an attested no-log gateway into confidential-compute inference on an open-weights model — the frontier-vendor residual applies only to rooms that choose the frontier trade. Honest position: the code is exactly this — readable, reproducible, and what AWS runs — and "it can't be watched while it runs" is now buildable, checkable, and in production for any Space the app builder runs in the enclave.


Read the render — the code that touches your plaintext

Verification is only worth as much as your ability to read what you're verifying. Plaintext opens in one place — the server-side render — and that surface is small: ~600 lines for the seal / turn / read core. It's published, readable, Apache-2.0:

the published render source

File Lines What it does
envelope.mjs 287 seal/open — a per-write key wrapped per recipient (room under HKDF(mk)); no operator recipient
asyncTurn.mjs 228 the decrypt-once turn: open ledger → append → model → append → re-seal → drop mk
spacePoll.mjs 79 the read: the sealed blob never leaves the server; unchanged = no decrypt; changed = new entries as plaintext
sessionStore.mjs 243 session memory, sealed with mk, fail-safe

That repo is a readable mirror; the hash-verified canonical is /source.tar.gz (Test 7), which rebuilds byte-for-byte to the deployed hash. Read the logic there; confirm the bytes here.


Setup (30 seconds)

  1. In the Spaces app, create a Space with "Who can post?" → Only specific Google accounts, and add any email to the allow-list. This is an admission:'email' Space.
  2. From its link …/space?room=sp-XXXX#mk=YYYY&gate=email, copy the room (sp-XXXX) and the mk (YYYY) — the room key that everyone with the link holds.
  3. Discover the API endpoint (it's public; the app fetches it the same way):
bash
SPACE=$(curl -s https://witbitz-spaces.pages.dev/space-config \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["spaceEndpoint"])')
echo "$SPACE"   # the /space Lambda URL the client posts to

You now hold exactly what a link-holder holds: the room and its key. The tests below show that is not enough.


A token-less poll — the read op — with a valid room key:

bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -w '\n%{http_code}\n' \
  -d '{"op":"poll","room":"<ROOM>","mk":"<MK>","sinceEtag":"","sinceCount":0}'
# → {"error":"not_authorized","reason":"unsigned"}
#   403

Holding mk returns no history. Reads require an allow-listed Google sign-in — the server won't decrypt-and-return without one.

Test 2 — the write gate: you can't post without an allowed identity

A token-less turn — the write op:

bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -w '\n%{http_code}\n' \
  -d '{"op":"turn","room":"<ROOM>","mk":"<MK>","message":{"text":"hi"}}'
# → {"error":"not_authorized","reason":"unsigned"}
#   403

Rejected before anything is written — nothing lands in the Space.

Test 3 — positive control: the key is valid; the 403 is the gate, not a bad key

Create a second Space with "Who can post?" → Anyone with the link (an open Space), and poll it the same way with its room/key:

bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -w '\n%{http_code}\n' \
  -d '{"op":"poll","room":"<OPEN_ROOM>","mk":"<OPEN_MK>","sinceEtag":"","sinceCount":0}'
# → 200  (returns entries)

Same request shape, a valid key — but no admission policy, so it reads. This is the control that proves Tests 1–2 are the admission gate refusing you, not a malformed request or a wrong key. An open Space reads by the link; an email Space does not.

Test 4 — the egress footprint: the app can only talk to a published allowlist

The other three tests check the server. This one checks the app itself: a static page can fetch() anywhere, so "the app doesn't leak your data to third parties" is a real claim — and it's now browser-enforced, not promised. Read the enforced Content-Security-Policy straight off the response header — it is the exhaustive list of hosts the loaded app is permitted to reach:

bash
curl -sI https://witbitz-spaces.pages.dev/space | grep -i '^content-security-policy:'
# default-src 'none'; script-src 'self' 'sha256-…' 'wasm-unsafe-eval' https://accounts.google.com; …
#   connect-src 'self' blob: https://api.witbitz.chat https://accounts.google.com; … frame-ancestors 'self'

Read it directly:

  • connect-src is the data path — where the app may send anything. It is exactly 'self' (the same-origin app + its map/tile proxy), https://api.witbitz.chat (the Space backend), and https://accounts.google.com (Sign-in). No analytics host, no CDN, no third party. Your data has nowhere else to go — the browser refuses it.
  • script-src allows only same-origin files and each inline block by its sha256no 'unsafe-inline'. So even injected script can't run, and can't exfiltrate.
  • default-src 'none' means anything not named above is denied by default; form-action and object-src are shut; frame-ancestors 'self' blocks cross-origin framing.

This is the egress-allowlist half of the signed certificate (the double blind §4–6) — now shipped and enforced. You are reading the app's true data footprint off ground truth. (What it does not prove is that the render is unobservable while it runs — that's the attested tier, now live as a per-room option (the attested tier). That the render binary is the published, reproducible build is proven separately by Tests 6–7. This test bounds the front-end; the gate tests bound access.)

Test 5 — the store holds only ciphertext, sealed to a key we never hold

The other tests check access and egress. This one checks what the operator actually holds. The sealed op returns the exact bytes the server stores for a room's chat — no key required, because reading ciphertext reveals nothing:

bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -d '{"op":"sealed","room":"<ROOM>"}' | python3 -m json.tool
# {
#   "alg": "AES-256-GCM · multi-recipient envelope v1",
#   "bytes": 14554,
#   "sha256": "ad7c09…",                                     ← hash of the exact stored bytes
#   "recipients": ["room"],                                  ← WHO can decrypt: only the room key. NOT the operator.
#   "sealed": "{\"v\":1,\"iv\":\"…\",\"ct\":\"…\",\"recipients\":{\"room\":{…}}}"   ← the opaque envelope itself
# }

Two things to read straight off it:

  • recipients is ["room"]. The envelope is sealed to exactly one key — the room key (mk), which lives in your link's #fragment and never reached the server (you didn't send it in the request above). The operator would be a recipient only if "beta" or "admin" appeared in that list. It doesn't. So the operator holds no key that opens this blob — an aws s3 cp, a backup, or a subpoena returns exactly these bytes: noise.
  • sealed is opaque. It's an AES-256-GCM ciphertext; without mk it is indistinguishable from random. Confirm it's genuinely your chat, not a decoy, by decrypting it locally with the mk from your link — the same key the server never saw.

This is content-blindness, made checkable: what the operator stores is ciphertext sealed to a key it does not have. (What it still can't prove: that the render — the one transient moment plaintext exists to run the model — doesn't secretly keep mk. That's declared-program-in-use, addressed next.)

Test 6 — the signed build certificate: what the platform DEPLOYED, committed in public

The last surface is the render code itself — where plaintext opens (to run the model on a turn, and to return the new entries on a changed read). Full proof (that the running binary is a faithful, un-tampered build) needs an attested tier (below). But the first step ships today: the platform publishes a signed certificate at /cert.json binding what it deployed to ground truth — a build identifier (git commit), the running Lambda's AWS CodeSha256, and the egress allowlist — and the running service reports the same identifier via op:attest.

bash
curl -s https://witbitz-spaces.pages.dev/cert.json | python3 -m json.tool
# { "signed": "{\"v\":1,\"service\":\"witbitz-space\",\"gitCommit\":\"…\",\"lambdaCodeSha256\":\"…\",\"egress\":\"…\"}",
#   "sig": "…", "alg": "ES256", "pubkey": {"kty":"EC","crv":"P-256","x":"…","y":"…"} }

curl -sX POST "$SPACE" -H 'content-type: application/json' -d '{"op":"attest"}'
# → { "service":"witbitz-space", "gitCommit":"<same sha>", "runtime":"aws-lambda" }

Verify the signature yourself — the platform's public key is published here, so compare it to the pubkey in the cert and check the signature over the exact signed string (ES256 / ECDSA P-256):

code
{"kty":"EC","crv":"P-256","x":"TFXrR5DFL3Oo3rBwa6clwJnrfvw0TN2AewDDEcy8hBM","y":"MgLLzEq7g8B86qS7NNxkIW64ytp296d1kkK1wSKBoec"}
javascript
// node — verifies the cert's signature against the published key (WebCrypto)
const c = await (await fetch('https://witbitz-spaces.pages.dev/cert.json')).json()
const ub = (s) => Uint8Array.from(Buffer.from(s.replace(/-/g,'+').replace(/_/g,'/'),'base64'))
const k = await crypto.subtle.importKey('jwk', { ...c.pubkey, ext:true, key_ops:['verify'] }, { name:'ECDSA', namedCurve:'P-256' }, false, ['verify'])
console.log(await crypto.subtle.verify({ name:'ECDSA', hash:'SHA-256' }, k, ub(c.sig), new TextEncoder().encode(c.signed)))  // true

What it proves: the platform has publicly, cryptographically committed to a specific deployed build (a named build id + the Lambda's exact code hash) with a specific egress. The commitment is tamper-evident (a changed cert fails the signature), and the running service reports the same build id — so the operator can't quietly point the certificate at one build while running another without it showing.

On its own, Test 6 is a tamper-evident commitment to a build. Test 7 turns it into a check of the actual code — the cert also carries sourceSha256, the hash of the published, readable source that reproduces lambdaCodeSha256.

Test 7 — the faithful build: rebuild the published source, get the deployed hash

The cert commits to a code hash. Test 7 lets you confirm that hash is a faithful build of source you can read — not a black box. The exact render source is published at /source.tar.gz; the cert's sourceSha256 pins it, and it rebuilds byte-for-byte to lambdaCodeSha256.

Needs only Node + Python 3 — no Docker. The zip is STORED (uncompressed), so the bytes don't depend on a zlib version; npm ci is deterministic for this pure-JS graph (no native binaries).

bash
# 1) download the published render source and confirm it's the one the cert names
curl -s -o source.tar.gz https://witbitz-spaces.pages.dev/source.tar.gz
python3 -c 'import hashlib,base64;print(base64.b64encode(hashlib.sha256(open("source.tar.gz","rb").read()).digest()).decode())'
curl -s https://witbitz-spaces.pages.dev/cert.json | python3 -c 'import sys,json;print(json.loads(json.load(sys.stdin)["signed"])["sourceSha256"])'
#   ↑ these two must match — the source you downloaded IS the source the cert commits to.

# 2) rebuild it and compare to the deployed hash
tar xzf source.tar.gz && bash tools/reproduce-space-build.sh
#   → reproduced CodeSha256: <base64>
curl -s https://witbitz-spaces.pages.dev/cert.json | python3 -c 'import sys,json;print(json.loads(json.load(sys.stdin)["signed"])["lambdaCodeSha256"])'
#   ↑ these two must match — you just rebuilt the exact bytes AWS is running (its CodeSha256).

Both match ⇒ the deployed render is a faithful build of code you just read. No trust in the build server: you reproduced it.

Honest scope — the one thing left. This proves the deployed code is a faithful build of readable source. It does not prove the running process is unobservable in use — that during the transient decrypt-and-render instant, the operator (or the cloud) can't read the plaintext out of memory. That is the attested/TEE tier (a confidential-compute enclave whose remote attestation binds the running measurement to this reproducible build). Production, and independently verifiable: see the attested tier. Everything up to and including "the code is exactly this, and it's readable and reproducible" is checkable today; "and it can't be watched while it runs" is now in production for any Space the app builder runs in the enclave — its per-room choice.


Test 8 — drive the real browser: prove the locks are enforced (the CDP verifier)

Tests 1–7 read a response — a header, a stored blob, a cert. That proves what the server declares, but not what your browser actually enforces on the live app — and a passive fetch is blind to a Service Worker, which can serve tampered bytes from its own cache with no network request at all. To close that, drive a real browser and check the app's claims from the browser's ground truth — below the page, below any Service Worker:

bash
node tools/lumen-verifier.mjs            # drives https://witbitz.chat/lumen  (pass a URL to point elsewhere)

It proves, from the running app rather than from a fetch:

  • the egress-lock is enforced — it runs a forbidden fetch() inside the room and confirms the browser blocks it with a connect-src violation (not merely that the header says so);
  • cross-origin isolation holds — the embedding site gets a SecurityError trying to read the room's DOM ("can't read a word");
  • no rogue Service Worker — it enumerates the browser-owned registration list, which a Service Worker cannot forge; the one thing a load-time or curl check can't see;
  • byte identity — the inline shell's SHA-256 is surfaced and confirmed to be one of the CSP hashes the browser pinned, so you can compare it to the reproducible source (Test 7).

Honest scope. It verifies the browser it drives — so run it yourself, or it's just checking someone else's browser (the "whose fetch" caveat, now at the browser level). It's a desktop tool; a stock mobile browser can't be driven this way — which is exactly what the native attested client (the attested client) is for: it is a controlled, bundled-local browser, so there's no rogue-worker surface and no external driver needed.


Reading the result codes

The gate is fail-closed; every rejection is a 403 not_authorized with a precise reason:

reason Meaning
unsigned no identity presented at all (no idToken + spk)
token: <…> a Google token was presented but failed — e.g. token: expired, token: bad_aud, token: bad_signature (RS256-vs-JWKS)
nonce_mismatch a valid token, but its nonce isn't bound to the signing key it was presented with
not_on_list a valid, key-bound token, but the email isn't on the Space's allow-list
revoked a valid, allowed identity that has since been removed from the Space
(none)200 accepted

How a real member passes (the signed path)

An allow-listed member's client sends, with each request, an idToken (a fresh Google ID token) and spk (its device signing public key). The server checks, in order: the token verifies RS256 against Google's JWKS; its nonce equals spkNonce(spk) = base64url(SHA-256(canonical-JWK(spk))) (so the token is bound to this device's key); and the email is on the sealed allow-list. Only then does it decrypt-and-return, or accept the write. Full mechanism: the Space link is the auth §4. This is the async analog of the live call binding an OIDC token to a live DTLS cert — "signed by the keyholder who authenticated as this account, now."


What you can't check here yet — and why

Being explicit, so this page doesn't become the overclaim it's meant to prevent:

  • Unobservable-in-use — the attested/TEE tier. The render decrypts with mk for a transient instant — on an AI turn and on every changed read, the one door plaintext opens. Tests 6–7 now prove the deployed code is a faithful, reproducible build of readable source — you can rebuild it and read it. The last thing they can't prove is that the running process can't be watched while it runs: that during that instant the operator (or the cloud) can't read the plaintext out of memory. That needs a TEE — a confidential-compute enclave whose remote attestation binds the running measurement to this reproducible build (and thereby excludes even the operator + AWS from the render). Live on real AWS Nitro hardware as a per-Space option — launch-on-demand, scale-to-zero (the attested tier); enabling it is the app builder's per-room choice. On that tier the model call also closes the last plaintext reader, the model vendor: it leaves the measured image only through an attested no-log gateway into confidential-compute inference on an open-weights model (fail-closed if that gateway does not attest) — shipped, with the frontier model kept as a per-room choice; your enclave check transitively covers that gateway's attestation.

This page grew "verify the gate" → "footprint" → "store" → "the build." The gate is live (Tests 1–3), the footprint is live (Test 4), the store is sealed to a key we never hold (Test 5), the signed build commitment is live (Test 6), and the faithful, reproducible build of published source is live (Test 7). Everything up to "the code is exactly this — readable, reproducible, and what AWS runs" is now checkable. What remains is only "and it can't be watched while it runs" — the attested/TEE tier, now live as a per-room option: turn it on and verify your Space against the running enclave's published measurement (above).


The Concierge — a real app, end to end

You've built a tenant, deployed a page, and verified the trust claims. This is the capstone: a complete reference app that puts it all together — a private evening Concierge that knows a great deal about its user, discloses only the minimum to an outside commercial service, and still works with it.

Get Started path: Vault SpacesDeploy itVerify itthis.


What you're building

One neutral page embeds two isolated iframes:

  • Your private Space — the branded Concierge, on your tenant. It knows the user (their city, their taste, their budget, who they're going out with) — all of it sealed in the vault, none of it on the page.
  • A commercial event finder — an ordinary third-party app with its own backend and its own data key. It knows nothing about the user.

The agent calls one host tool, search_events. Only a minimal, neutral query crosses the boundary — city, date, a broad category, an attendee count — never a name, never the conversation. The commercial side returns real events; the agent gets them back in the same turn and recommends the one that fits the private context. That is capability-mediated disclosure: the app that holds the data discloses a capability, not the data. The seam is a single origin-checked postMessage — see Embed a Space and Tools & widgets.

The app is otherwise ready to run. Two pieces of config — one on each side of that boundary — bring it up.

1. Get a key from SeatGeek (the commercial side)

The event finder is a normal app with its own data source. Get a free SeatGeek Platform client:

  1. Sign up at seatgeek.com/account/develop and create an app.
  2. Copy the Client ID — the short NjA2… string. Not the 64-hex client secret: that's for OAuth flows this app doesn't use, and SeatGeek rejects it with 403 Invalid client credentials.
  3. Set it as a secret on the commercial app — SEATGEEK_CLIENT_ID (a Cloudflare Pages secret, or an env var wherever that page's backend runs) — and redeploy so it's picked up.

This key lives entirely on the commercial side — Witbitz never sees it, which is the whole point: the outside service keeps its own credentials, the private Space keeps its own, and neither reaches across. Without a key the reference adapter falls back to deterministic mock events, so you can watch the flow before you wire real data.

One SeatGeek quirk worth knowing: its free-text q is a literal title match, so a genre word like jazz returns almost nothing. Pass genres as the category (the reference adapter maps them to SeatGeek's type param) and let the agent pick the fitting act out of the real list it gets back.

2. Add your origin to your tenant

Your Concierge creates tenant-attributed Spaces, and the data plane is fail-closed: a tenant admits a member's browser only from an origin it has declared. Until you add yours, every read (poll, state, turn) is refused — the room stamps to your tenant, but nothing loads. This is the participation gate (Identity & admission); confidentiality is still the room key, always.

Declare the origin the Concierge is served from, authenticated with your tenant secret key:

code
PUT /v1/tenant/webhooks
Authorization: Bearer <your tenant secret key>

{ "origins": ["https://your-concierge.example.com"] }

Use the exact scheme + host the Concierge iframe loads from — that's the Origin the platform checks — and add every origin you serve it from (a preview alias and production are two distinct origins). You can also pass origins when you first register the tenant. See the Platform API for the full /v1/tenant surface.

That's it

Load the page. The Concierge auto-creates its Space on your tenant — every room now carries your tenantId, so the rooms it makes are inventoried and reclaimable, not anonymous (Vault Spaces). Ask it to find something to do, and watch the neutral query — and only that — cross to the event finder, real events come back, and the agent recommends from your side of the wall: the one recommendation the event service could never make, because it never learned who the user is.

Next

The Concierge now also runs as an app-faced Space — the Event Finder full-screen with the room as a pull-up card, including per-room resume and the sign-in patterns: see App-faced Spaces.


App-faced Spaces

A Space can wear an app: the full-screen surface is your app site, and the private room rides over it as a pull-up card — the input line sits exactly where a normal Space's does. The agent's tools execute against the app pane over an origin-pinned bridge, and the card's grab bar shows exactly the structured fields that crossed to your site — nothing else ever does. The reference is the Event Finder concierge: a Space whose "chat history area" is a live commercial events site.

Status: the app-site mode and the wire contract below are live (the Event Finder demo runs on them). The sign-in patterns in §3 are documented designs — the recommended shapes, not yet shipped primitives.

1. Declaring an app site

A branded deployment declares its app in the brand config:

json
{ "agentName": "Concierge", "persona": "…", "appSite": "https://your-app.example", "tools": [  ] }

Opening a Space on that origin renders appSite full-screen with the room as the card. Every Space on the origin gets the app face, the brand's tools, and per-room resume: the last search is stored in the room's own sealed state doc, so returning to a room (any device, any member) replays it.

Three gates for a new app-site origin, all fail-closed:

  1. the Spaces app's CSP must allow framing it (SPACE_VAULT_APP_ORIGINS at deploy);
  2. the app site's frame-ancestors must list the whole ancestor chain (the Spaces origin, and the vault origin if the app runs vaulted — browsers check every ancestor);
  3. the app site should origin-pin inbound messages to the Spaces origin(s).

2. The wire contract

The pane speaks two messages down, one up — all postMessage, origin-pinned both ways:

direction message meaning
Space → pane { type: "search", args } run a query; args are exactly the disclosure-strip fields
Space → pane { type: "focus", index } highlight the Nth already-shown result
pane → Space { type: "results", events: [...] } the public results — returned into the agent's turn, so it recommends from live data

The pane sees only these messages. It never receives the conversation, names, or reasons — the same-origin policy walls it off, and the bridge carries nothing but the structured arguments the strip displays.

3. Signing into the app pane

Three patterns, in ascending capability. A key browser fact shapes all of them: the pane is an iframe, so it cannot see your normal first-party login cookies (third-party cookies are blocked or partitioned), and most authenticated sites refuse framing entirely — an arbitrary site can't be pointed at and inherit your session. A cooperating app site, however, has clean options.

3a. No sign-in (public data) — what the demo ships

The pane serves public data keyed by nothing. Simplest, fully anonymous toward the site: it learns only the strip's fields per query. Right default for discovery-type apps.

3b. Personal account (per member)

The standard embedded-app pattern (the same shape Spaces' own central auth broker uses):

  1. the pane opens its login in a popup — top-level, so cookies are first-party and Google/password/passkeys work normally;
  2. the popup hands the session back to the pane, which keeps it in partitioned storage (a Partitioned cookie or a token in the iframe's storage). Partitioned storage is stable per embedding origin, so you sign in once per device and stay signed in across rooms and reloads.

What it buys: account prices, saved payment, a "buy" that completes. Disclosure honesty: the site can now join your queries to your identity — the strip should say so (shared → … · as you@example.com). Each member is signed into their own account, so panes may show member-specific views, and tool results come from whoever asked.

3c. Shared room account (a "party" account) — the room-native pattern

If all members use one stateful account, the site's own backend becomes the room's shared surface: one cart, one favorites list, one booking — every member's pane converges by definition, with no extra sync protocol.

The elegant part is how the account is shared: never a password. The site issues a scoped session token (a party token), and the token rides sealed inside the room — in the same encrypted per-room state doc that already carries the resume search:

  • Invite to the room = access to the account. A new member's pane receives the token over the bridge and is signed in with zero ceremony. The room key already gates who's in; the site token inherits exactly that boundary.
  • The platform never sees it — the token lives in the mk-sealed doc, end-to-end encrypted like the chat.
  • Revocation = rotate the token when membership changes (the same epoch model a membership change already implies).
  • The site sees one identity doing minimal structured queries; the strip shows as <party account>.

Caveats: one identity for N people (right for a family or a trip crew, wrong where per-person accountability matters); concurrent edits are the site's job (carts handle this well); token rotation must accompany member removal, or a removed member's pane keeps working until expiry.

Choosing

site learns shared state best for
3a public queries only none (per-pane) discovery, demos
3b personal queries + who you are per member purchases, personal accounts
3c party token queries + one shared identity the site's own state, room-wide planning together: shared carts, group bookings

Next


Platform API — the /v1 surface

The tenant-keyed HTTP API a third-party app builds on. JSON in, JSON out, over HTTPS. This page is the map; the machine-readable spec is canonical — point tooling at it:

  • Production spec (only ops callable today): https://api.witbitz.chat/v1/openapi.json (.yaml)
  • Full design spec (every op, x-status-labelled): https://api.witbitz.chat/v1/openapi.full.json
  • Discovery index: https://api.witbitz.chat/v1/

Base + envelope

  • Base URL: https://api.witbitz.chat/v1
  • Success is enveloped { "data": <payload>, "meta"?: {...} }; errors are { "error": { "code", "message", "request_id"? } }. One exception: POST /space (x-envelope: false) returns a raw payload and a flat error shape — unifying it is a tracked follow-up.
  • Version 1.0.0-beta. Private beta — the contract may change (coordinated with invited integrators) until GA.

Authentication

Two tenant API keys, resolved by the /v1 authorizer to a tenantId:

  • Secret key wsk_ — full scope, server-side only.
  • Publishable key wpk_ — browser-safe, public reads + funnel-entry writes. It identifies a tenant; it does not authenticate an end user.
  • Per-collection capability tokens (x-collection-token): the owner admin_token or the add-only contributor_token, for owner/contributor actions on a collection. Wallet/license credentials travel in the request body, never a URL.

Capability tokens + raw keys are shown once at creation and stored only as a hash (or KMS-sealed). Store them server-side; lost = re-create.

Operational contract

  • Idempotency: money/billable ops (POST /sessions, /wallets/grant, /collections/{id}/paint) require an Idempotency-Key; same key + same body replays the first 2xx byte-for-byte.
  • Rate limiting + kill switch: throttled at the gateway; per-tenant/per-key-type limits at the authorizer; an operator can suspend a tenant (POST /tenants/{tenant_id}/status) — every key is denied within ~15s. That op and POST /tenants are operator-only (X-Admin-Token, not a tenant key) and are deliberately absent from the published spec, so don't go looking for them in openapi.json.
  • Secret handling: one-time-secret responses are Cache-Control: no-store; sensitive fields x-sensitive; BYOK provider keys + the webhook secret are KMS-sealed.

The surface (by area)

Agents - GET /catalog — the public agent catalog (publishable).

Collections — the generic content primitive (opaque metadata + items + curation + a keyless render page) - POST /collections · GET /collections/{id} · DELETE /collections/{id} - GET /collections/{id}/renderkeyless share page for published content (public) - items: GET/POST /collections/{id}/items · PATCH/DELETE /collections/{id}/items/{itemId} - POST /collections/{id}/state — merge opaque app state onto the collection

Artifacts — produced media on a collection - GET/POST /collections/{id}/artifacts · GET/PATCH/DELETE .../artifacts/{artifactId} - POST /collections/{id}/paint · POST /collections/{id}/produce - POST /collections/{id}/artifacts/upload → presigned S3 PUT, then POST /collections/{id}/artifacts/{artifactId}/ready to stamp it complete — both halves are required

Sessions - POST /sessions — launch (or re-summon) an agent; metered, idempotent. Accepts either key: the spend credential is the coupon in the body, so a publishable key launches nothing without a funded wallet.

Credits - POST /wallets/lookup · POST /wallets/grant · POST /checkout · GET /usage - POST /wallets/redeemtwo presentations: a Lemon Squeezy license_key, or a blinded issuer {token, sig, epoch}. Both mint a wallet_code shown once. For a token the token IS the idempotency key — a retry gets 409 already_spent, never a second wallet, so don't send an Idempotency-Key. - POST /epochsissuer role required. Publish a batch's PUBLIC key and per-token value, after which tokens signed by the matching private key are redeemable. The platform holds only public keys, so it can verify what an issuer signed and can never mint on its behalf. Set once: rotation is a new epoch.

Roles. A tenant is an app (builds a product, spends credit) or an issuer (holds the customer and the payment relationship, funds wallets from its own prefunded balance) — or both. grant and /epochs are issuer-only. A tenant with no roles recorded predates the split and is treated as holding both. Roles are set by an operator, not self-service.

Tenants (onboarding + key management) - POST /tenants/signup · POST /tenants · POST /tenants/{tenant_id}/status - GET /tenant · GET/POST /tenant/keys · DELETE /tenant/keys/{keyId} - PUT /tenant/provider-keys (BYOK, KMS-sealed) · PUT /tenant/webhooks

SpacesPOST /space, the async Space's single op-dispatched endpoint. It is in /v1 but keyless: it authenticates by key possession, not a tenant key (see the note below).

InfraGET /turn (ephemeral TURN credentials).

Not at /v1: ratings, /agents/*, /earnings and /sessions/{id} are x-status: legacy — they exist on the older surface but are not routed here, and calling them returns 404. They appear in openapi.full.json labelled as such, never in the production spec.

Access

Invite-only during private beta. Email hello@witbitz.chat for keys. Everything above is served openly as a contract; the keys are what's gated.

Notes

  • The async Spaces runtime (/space) is a separate, content-blind surface (key possession, not tenant keys) — see async Spaces and the Space link is the auth.
  • This page is a summary; the OpenAPI at /v1/openapi.json is the source of truth (schemas, per-op auth, examples).

The SDK — a Witbitz app in an afternoon

Witbitz is a runtime: a shared room with humans, an AI agent, sealed persistent state, identity and authority. The SDK is the small, stable surface over it. The trust machinery — the room key that never reaches the server, content-blind reads, the op-dispatch, the poll loop — disappears, so a static page can stand up a private, agent-backed, link-shared room in a few lines.

Status: public beta. Usable today from any static page; the surface below is small on purpose and meant to stay stable. It wraps the exact reference client the production Spaces app runs on — nothing is forked or reimplemented.

The whole thing

html
<script type="module">
  import { Witbitz } from 'https://witbitz-spaces.pages.dev/witbitz-sdk.js'

  const wb = new Witbitz()

  // A private, agent-backed room. The link IS the auth — share it to let someone in.
  const space = await wb.createSpace({
    agent: { name: 'Helper', instructions: 'You help a small group plan and decide together.' }
  })

  space.on('message', (m) => console.log(m.from, m.text))   // yours, others', and the agent's — all arrive here
  await space.send('Plan us a weekend in Kyoto')            // the agent answers on its own turn → via on('message')

  console.log('Share this:', space.shareLink)
</script>

That is a complete, multiplayer, private AI application. No backend of your own, no key handling, no database.

The surface

Three calls. That is the whole API.

new Witbitz({ endpoint?, viewer?, app? }) A client bound to the Space endpoint. Make one, reuse it. Defaults to the public production endpoint.
await wb.createSpace({ agent, tools?, immediateTools?, model? }) Mint a Space and return a live Space. agent = { name, instructions, tools?, model? }.
await wb.openSpace(link) Join a Space from a share link (or the current page URL, if it is one). The link's fragment carries the key; it never touches the server.

A Space:

space.shareLink The link to hand someone — the link is the admission capability.
space.room The server-visible room id.
space.on('message', cb) cb({ from, text, self, id, ts, widgets? }) for every entry — yours, other members', and the agent's (self: true). On an opened Space, the existing history replays here first.
await space.send(text, { from? }) Post a message. The agent decides whether to answer and does so on its own turn; the reply arrives via on('message'), not as a return value.
await space.sealedProof() What the server actually stores for this room — the opaque, sealed ledger, plus its size, hash, and who can decrypt it (the operator never can). Lets your app prove the operator can't read the store, not just claim it. null before the first message.
await space.privateLane({ agent, actions? }) Attach a private assistant lane for the current member — see Private lanes and human-approved actions. → a Lane.
space.on('error', cb) Transient poll errors (non-fatal; the loop keeps going).
space.close() Stop streaming and release the handle.

What you get, and what's handled for you

Standing up a Space gives your app, for free:

  • Privacy that's verifiable, not promised. State is sealed to a room key the SDK derives on the client; the server stores only its commitment and never retains the key. The SDK reads via pollsealed, so mk stays on the device for reads — it opens the sealed ledger locally. An agent turn is the one exception: it hands mk to the server-side render for the transient decrypt-once that runs the turn, then the render drops it — that single instant is what the attested tier closes. See Async Spaces, Trust model, and Verify it yourself.
  • An agent that participates, not one that rules. The agent answers turns and can use tools, but high-stakes actions become proposals a human approves — it never holds the credential. See Delegated authority.
  • Admission in the link. The share link is a self-contained capability; there is no account server to sign up against. For allow-listed rooms, add an email gate (see below). See The Space link is the auth.
  • Idle-cheap persistence. A quiet Space is sealed bytes on disk, not running compute; the agent is a function invoked on a turn, not a daemon. See Async Spaces.

You never touch: the room key, envelope sealing, the /space op-dispatch, the poll/etag loop, or endpoint resolution.

Tools and human authority

Give the agent platform tools by name; the ones in immediateTools it may call directly, the rest become proposals a member approves before they run.

javascript
const space = await wb.createSpace({
  agent: { name: 'Travel', instructions: 'Plan trips; put options on the shared map and list.' },
  tools: ['search_places', 'show_places', 'add_place', 'search_flights', 'show_flights', 'set_itinerary'],
  immediateTools: ['search_places', 'show_places', 'set_itinerary'] // read/show freely; anything effectful is a proposal
})

The full catalogue (places, flights, itinerary, chart, photo, read_file, read_url, write_pdf) is in Tools and widgets.

Private lanes and human-approved actions

Attach a private assistant lane to a shared Space with space.privateLane({ agent, actions }). The lane is the member's own room: its agent sees the shared room's messages and thinks/drafts with the member privately, but can post into the shared room only through a crossing the member approves. Give it actions, and it can only ever propose them — nothing runs until a human approves, and the action then executes on the member's device from your own handler. The agent never holds the credential.

javascript
const shared = await wb.createSpace({ agent: { name: 'Team' } })

const lane = await shared.privateLane({
  agent: { name: 'Ops assistant', instructions: 'Help me run the store. Propose refunds and emails; I approve.' },
  actions: [
    { name: 'issue_refund', description: 'Refund a customer',
      input: { type: 'object', properties: { customer: { type: 'string' }, amount: { type: 'number' } }, required: ['customer', 'amount'] },
      run: (a) => myBackend.refund(a.customer, a.amount) },   // ← runs ONLY after the member approves
  ],
})

lane.on('message', renderPrivateThread)
lane.on('proposal', (p) => showApproveDeny(p))    // { id, kind:'message'|'action', action, args, text, approve(), deny() }
lane.on('resolved', ({ approved, action }) => { /* … */ })
await lane.send('Refund Jane $40 for the late order, and email her an apology')

A Lane is a Space (its private thread streams on message), plus:

await lane.send(text) Talk to your assistant privately. Anything it wants the shared room to see, or any effectful action, it must propose.
lane.on('proposal', cb) cb({ id, kind, action, args, text, approve(), deny() }). kind:'message' is a drafted crossing to the shared room; kind:'action' is one of your actions. Nothing happens until approve() (posts the crossing / runs your run handler on the device) or deny().
lane.on('resolved', cb) cb({ id, approved, action, result? }) after a decision.

Effectful actions execute on the member's device from your run() handler — the runtime records the approval but never runs them itself, so the credential and the side-effect stay on your side. This is the delegated-authority model (Delegated authority), one method call away. It's the piece that's genuinely hard to build safely by hand: a propose→approve→execute state machine, an audit trail, and a hard guarantee the agent can't act unilaterally.

Configuration

  • endpoint — the Space API. Defaults to the public production endpoint (https://api.witbitz.chat/v1/space), a self-authenticating endpoint: a Space proves itself by its key-commitment, not a tenant account.
  • viewer — the origin a shareLink opens in. Defaults to the reference Spaces app, so a link Just Works. Point it at your own origin to open Spaces in a viewer you build (the SDK also exposes space.key so you can construct links yourself).
  • app — an optional tag recorded with the Space.
  • On-prem / your own model, keys, and IdP: point endpoint at your deployment. See Deployment options and On-prem.

Try it

A complete example app (create-or-join a shared assistant room, ~40 lines) is live at witbitz-spaces.pages.dev/sdk-example.html — open it, then open the share link in a second tab to see the same sealed room from another "member." Its source is sdk-example.js on the same origin.

A second example shows what the SDK uniquely makes easy — The Confidential Room: talk to an AI advisor about something you'd never paste into an ordinary chatbot, while a live panel fetches space.sealedProof() and shows the server holds only an opaque envelope, sealed under a key it never receives. Building this safely without Witbitz means end-to-end encryption, a key model where the server can't hold the key, and a way for a user to audit it — here it's the runtime, and the proof is one method call. A normal AI product asks you to trust it won't read your data; this one lets a user check.

And The Ops Desk shows human-in-the-loop authority: a private assistant that can draft real actions — refunds, emails — but can never run one on its own. Each is a proposal you approve, and only then does it execute, on your device, from your handler. The agent never holds the credential. It's a privateLane({ actions }) plus on('proposal') + approve().

Notes and limits

  • The SDK is a single ES module; cross-origin import from the URL above pulls its dependencies from that origin automatically, so your page's CSP must allow it (script-src and connect-src for your endpoint).
  • The reference viewer is the fastest way to see a Space; a production app typically builds its own UI with on('message') + send, exactly like the example.
  • Email-gated admission, private per-member lanes, and running a Space inside the attested enclave are supported by the runtime; the thin surface above covers the open-room path first. See Identity and admission and The attested tier.

Read next: Vault Spaces to build with the reference Spaces, and Platform for the runtime this rides on.


Embed a Space — one line, and the drop-in doesn't cost you the encryption

Status: live. The host-side SDK is served at https://witbitz.chat/embed.js; the iframe seam ships in the Spaces app. Live demo → witbitz.chat/demo: talk to an embedded AI and it calls the page's own set_site_color tool to restyle the site — your app's functions become the agent's tools. A simpler read-only event-log demo (the bridge events, no host tools) is at /site/embed-demo.html. Cross-origin, browser-verified.

The one-liner

html
<script src="https://witbitz.chat/embed.js"></script>
<witbitz-space link="https://witbitz.chat/space#<your link>"></witbitz-space>

or drive it from script:

javascript
const space = Witbitz.embed('#slot', { link })
space.on('message', m => )   // someone posted (metadata only)
space.on('turn',    t => )   // an agent turn finished
space.signIn(token)           // owner-gated rooms: admit the user at their tier

Witbitz.embed(target, opts) returns a controller: on(type, cb) / off, signIn(token), navigate(to), setTheme(theme), destroy(), and iframe. <witbitz-space> re-exposes it as el.space.

Why it's an iframe — and why that's the point

That one line mounts a cross-origin iframe on the Spaces origin, not inline DOM in your page. That isn't a limitation to apologize for — it's what keeps the guarantees true when a Space is embedded:

  • The verified Space client runs in its own origin. Your page — and every tracker and third-party script on it — cannot reach into it, cannot read the room key, cannot read the plaintext.
  • If a Space were inlined into your page, your page would become part of its trusted computing base, and content-blind would quietly stop being true the moment anyone embedded it. The iframe is the isolation boundary.

So a drop-in embed does not cost you the encryption — the part that touches your users' content still runs only in verified code, in a context your page can't see.

The bridge (host ⇄ Space)

The only channel across an origin boundary is window.postMessage. The SDK layers a small, origin-checked protocol (JSON-RPC 2.0) on it. It is a narrow capability seam, not shared memory — the host can do exactly these things:

Direction Message Meaning
Space → host ready the Space handshaked; the bridge is open
Space → host message someone posted — metadata only ({id, self, sys}), never content
Space → host turn an agent turn finished
Space → host member a member event
Space → host resize content height changed (auto-sizes the iframe)
host → Space signIn hand in an owner IdP token / signed grant
host → Space navigate / theme deep-link / theme within limits

Discipline that makes the seam safe: origin-checked both directions (the Space only accepts control from the one validated embedder; the host trusts only the Spaces origin), nothing is posted to '*', and the room key never crosses the bridge — it rides the iframe's URL fragment, client-side, and is never sent to a server. Sign-in is a verified token, not a trust-me flag: it's checked against the room's sealed allow-list and resolved to a tier (see the owner rule).

One honesty note: the app that embeds a room necessarily holds that room's link, so it holds that room's key — the isolation doesn't hide the embedded room from the embedder. What it protects is (a) the platform-wide guarantee that Witbitz can't read content, and (b) the room from your page's other scripts and trackers.

Who may embed — two gates

Embedding is off by default (frame-ancestors 'self'). Turning it on is two layers, deliberately:

  • frame-ancestors — the app-wide COARSE gate (browser-enforced). It has to be coarse: the browser evaluates the header before any JavaScript runs, when the room isn't even known (especially for #mk= fragment links). Set the allowed embed origins for a deployment via SPACE_EMBED_ORIGINS (it feeds both this header and the client check from one source, so they can't drift).
  • The owner policy — the per-room PRECISE gate (client-enforced). The owner's signed AppPolicy may name embedOrigins — the origins allowed to embed that app's rooms. Because the client holds the room key, it reads the sealed, signed policy itself and closes the bridge if this embedder isn't on the list. The server never sees it — content-blindness is preserved. On the hosted multi-tenant platform the owner key is resolved from the tenant registry (op:'policy'); on a pinned single-owner deployment it's the baked key.

So the browser stops any non-registered origin from framing at all; the owner policy narrows it to this owner's origins per room. Fail-closed.

Orthogonal to what the Space is

The embed is transport + isolation. Whether the client is trustworthy code is a separate axis — the attested client; admission and tiers are the owner rule. They compose: an embedded Space is still content-blind, still owner-governed, still verifiable.

One line: witbitz.chat/embed.js drops a real, isolated, content-blind Space into any page, talks to it over a narrow origin-checked bridge that never carries your plaintext, and lets the room's owner decide who may embed it.


Run an agent turn

An async Space does not keep an agent process alive. A member posts a turn; the render opens the sealed room state, runs the agent, writes the reply, and goes cold again.

That is the core runtime loop.


The turn flow

One request-scoped render
Member sends turn with room key and message
->
Render checks admission, opens the ledger, runs tools, re-seals, and drops the key
  1. A member sends { op:'turn', room, mk, message }.
  2. The transport checks commit(mk) against the Space record.
  3. If the Space is gated, identity is verified against the sealed admission policy.
  4. The render opens the sealed ledger.
  5. The member entry is appended.
  6. The agent runs with the Space's persona and tools.
  7. The reply, widgets, files, or pending proposals are appended.
  8. The ledger is re-sealed and stored.
  9. The room key is dropped.

Read the deeper version in Async Spaces.

Minimal turn

javascript
const r = await client.turn(room, mk, {
  from: 'Ada',
  text: 'Summarize our plan and list the unresolved decisions.'
})

For an open Space, that is enough.

Signed member turns

A signed turn proves which admitted member wrote the entry. The client signs the entry core and sends the signature with the message. For an email-gated Space, the message also carries an ID token and the member signing public key.

Conceptually:

javascript
await client.turn(room, mk, {
  text: 'I approve the itinerary.',
}, {
  email: 'ada@example.com',
  googleIdToken,
  sign: { priv: memberPrivateKey, pub: memberPublicKey }
})

The render verifies the identity and stores only the verified attribution, not the raw ID token.

When the agent answers

A turn always lands in the ledger. Whether the agent replies is a separate decision, controlled per Space from the composer:

Mode Behaviour
always Reply to every message.
auto (default) Reply if the Space is solo or the message addresses the agent. In a group message not aimed at it, a cheap model gate decides whether the message wants help.
ondemand Reply only when addressed by name or @-mention; otherwise the message is stored silently.

A silent turn is not a failure: it returns silent: true with the entry count, so the client can drop its "thinking" indicator immediately rather than waiting for a timeout.

Two things override the mode. A venue that hard-mutes the agent (agentReplies: false — an observer room) always wins. And if the chime-in gate cannot be reached, it fails open and the agent answers: an unavailable model must not silently turn a group Space mute.

The gate is the reason a Space's tests should not depend on ambient model credentials — with a key present it makes a real call, and the model's verdict then decides whether a reply exists.

Tools during a turn

A Space's tools are declared in its sealed config. During a turn, the agent can call those tools. Tool outputs may become:

  • plain text in the reply
  • a shared widget such as a map, chart, diagram, itinerary, or flights card
  • a file attached to the reply
  • a pending proposal requiring human approval

High-stakes calls are not run directly. They become proposals under Delegated authority.

Reads after a turn

Clients normally watch a Space with pollSealed. When the sealed ledger changes, the client opens it locally and renders the new entries.

javascript
const r = await client.pollSealed(room, mk, previousEtag, previousCount)

For gated Spaces, read operations also include the caller's identity so the server can refuse unauthorized reads before returning sealed content.

What plaintext exposure means

In the current production tier, the server-side render receives mk during turns. That is why the model can reason over the Space. The key is request-scoped and dropped afterward, and storage remains sealed to your key.

This is not the same as "the operator can never see plaintext in use." The attested server tier makes that stronger property checkable, and it is now in production: turn a Space's enclave on and its turns run inside measured Nitro hardware with the key sealed to that image. Enabling it is the app builder's per-room choice.

Read: Status, The double blind, The attested tier

Code map

Concern File
HTTP turn op agent/spaceHandler.mjs
Runtime service agent/spaceService.mjs
Decrypt-once render agent/asyncTurn.mjs
Agent tool loop agent/brain.mjs, agent/brainTools.mjs
Delegated authority agent/delegatedAuthority.mjs
Client turn wrapper spaces/public/spaceClient.js

Tools and widgets

In Witbitz, tools are how an agent reaches capability, and widgets are how tool results become shared application state. The important distinction is that a widget is not one user's private chat decoration. It is part of the Space that every member can see.


Tools are declared by the Space

A Space's sealed config names the tools available to the agent:

javascript
await client.create({
  agentName: 'Planner',
  persona: 'Plan trips with the group and keep decisions clear.',
  tools: ['search_places', 'show_places', 'show_chart', 'write_pdf']
})

The render opens this config during a turn and exposes the declared tools to the agent. If the config is sealed, the platform cannot read the persona, tool list, admission policy, or model config.

The built-in catalogue

These are the tools a hosted Space can name in its config today. A Space only gets what it declares — an empty tools list is an agent that can talk and nothing else.

Tool What it does Produces
search_places Query Google Places for candidates. Text — the agent then picks
show_places Put chosen places (by id) on the shared map. places widget
add_place Add one place to the existing map doc. same places widget
search_flights Query Duffel for real offers. Text — the agent then picks
show_flights Put chosen offers (by id) on the shared card. flights widget
set_itinerary Replace the whole day-by-day plan. itinerary widget
add_to_itinerary Append to the existing plan. same itinerary widget
show_chart Draw data as SVG. chart widget
show_diagram Draw a relationship graph as SVG. diagram widget (same page)
show_photo Put an image in the reply. Sealed out-of-line blob
read_file Read a shared PDF / Word / Excel / CSV. Text
read_url Fetch a public URL (SSRF-guarded). Text
write_pdf Generate a PDF from Markdown. File on the reply

Query-then-show is deliberate. search_* returns candidates to the agent; a second call puts the chosen ids on the shared doc. The member sees a curated set, not a raw result dump, and the agent's choice is the part that gets sealed into the room.

Two widget kinds are not agent-produced: location (a member shares live location from the composer) and nav (turn-by-turn, opened from a place card). The client can also render trip and shop docs, which no current first-party tool emits.

Any shared view can be opened full-screen from the room's menu — a widget is a living document, so reaching it must not depend on scrolling back to the message that first posted it.

Tool results become room artifacts

Tools can return different kinds of results:

Result Example
Text A searched page summary or calculation result.
Widget A map, chart, diagram, itinerary, route, or flights card.
File A generated PDF attached to the agent reply.
Image/blob A sealed out-of-line photo or generated image.
Proposal A high-stakes action waiting for human approval.

The agent reply can carry pointers to widgets or files created during the same turn. The client renders those pointers as live shared UI.

Shared widget state

The state op stores a named shared widget document. A member or tool can set a patch and summary; later reads return the latest state. The state is sealed with the room key.

javascript
await client.state(room, mk, 'trip-plan', {
  days: [
    { name: 'Day 1', items: [{ text: 'Arrive and check in' }] }
  ]
}, 'One-day starter itinerary', 'Ada')

Because the document belongs to the Space, every member sees the same widget state.

High-stakes tools become proposals

An agent should not directly run actions that spend money, change external systems, or cross a policy boundary. Instead, the turn records a proposal. A human later approves or denies it:

javascript
const pending = await client.pending(room, mk)

await client.decide(room, mk, pending.proposals[0].id, true, 'Ada')

Only an approved proposal executes, and only once.

Read: Delegated authority

Air-gap behavior

When AIRGAP=1 is enabled in an on-prem deployment, internet-touching tools are refused server-side. Local tools such as chat, document reading, PDF generation, charts, and diagrams continue to work. Map/photo/search/flight tools need either local replacements or must stay disabled.

Read: Enterprise and on-prem

Code map

Concern File
Tool declaration and validation agent/affordance.mjs, agent/agentRecord.mjs
Space tool execution agent/spaceHandler.mjs, agent/spaceService.mjs
Shared widget docs agent/sessionStore.mjs, spaces/public/spaceClient.js
Widget renderers spaces/public/*Widget.*
Delegated actions agent/delegatedAuthority.mjs

See also — the inverse direction: a widget puts your app's UI inside a room; Embed a Space puts a whole room inside your app's page (a cross-origin iframe + an origin-checked bridge, content-blind either way).


Async Spaces — the agent is a function, not a server

The first Witbitz Spaces are async: a persistent room that lives over weeks, where the AI is a function invoked per turn, not an always-on process. Between turns the Space is cold ciphertext — so an idle Space costs ≈ $0. This is the model the Spaces app runs on. Live in production.

See also: the Space link is the auth (membership + identity) and the double blind (why the platform can't read a Space).


1. Agent-as-function

A live call runs the agent as a container for the call's whole life. An async Space instead invokes a turn function only when a present member acts:

A member posts → the transport invokes the turn with the member's mk → the function opens the sealed ledger, asks the model, appends the reply, re-seals, stores, and drops mk. Then the Space is cold again.

No held socket, no always-on compute → idle ≈ $0 (the reason to prefer poll/push over held connections; cost model in the repo's async-rooms-cost.md). It stays on AWS (DynamoDB ledger + S3 blobs + Bedrock/LLM in-boundary) so the ciphertext and the inference share one trust boundary.

2. The sealed ledger

A Space's conversation is one snapshot sealed under mk and stored as ciphertext (sessionStore.putLedger/getLedgeragent/envelope.mjs seal/open). It is sealed to the member key only — there is no operator recovery key. The platform holds no key; to it, a Space is opaque bytes. This is the persistent Space memory.

How it is sealed. v2 re-seals the whole ledger on every turn — simple, and O(n) in the history. v3 appends the new entries as per-entry sealed boxes, so appending is O(1) in the delta and a reader can take a content-blind slice without opening everything. v3 is what production runs; the writer is flag-gated (SPACE_LEDGER_V3) and migrates on write, so the two formats coexist and the change is reversible.

3. The turn — a decrypt-once render

agent/asyncTurn.mjs runAsyncTurn is the render, and it is the one place plaintext exists:

  1. access controlmk arrives from a present member's client; agent/spaceTransport.mjs checks commit(mk) === the Space's stored commitment (proves membership + that this turn seals to the ledger's key).
  2. attribution (for a signed Space) — verify the turn's signature against the member grant, set the verified author (room-link-auth §4).
  3. decrypt-once — open the sealed ledger, append the incoming entry, run the brain (its tools may produce widgets/artifacts), append the reply, re-seal, putLedger.
  4. drop mk — the key is never held past the turn.

Between turns nothing runs and nothing is readable.

4. Reaching members — poll, content-blind

agent/spacePoll.mjs is the read side, and it stays blind on the common path:

  • The client sends its last etag; the server computes etagOf(sealed) = a hash of the ciphertext — it reveals only "changed", never the turn count, and it is computed without decrypting.
  • Unchanged → return no entries and never decrypt (mk unused).
  • Changed → decrypt-once, return only the plaintext new entries after the client's cursor, drop mk.

The committed ledger's plaintext is produced server-side — the client is thin: it sends turns and receives server-decrypted plaintext. (Its only crypto is minting the key, opening the small in-flight partial, and the optional pollsealed read below.)

Away members are reached by push (agent/webPush.mjs, ops subscribe / unsubscribe). The notification is a content-free tickle: it carries no message text, only "something happened in this room". The service worker wakes and polls — receiving server-decrypted plaintext, exactly as an in-app read — so the push provider learns nothing a poll would not already reveal; content-blindness against the push provider survives the notification path. This works in an installed iOS/Android home-screen PWA, with no native app.

Reads are gated too, for a private Space. On an admission:'email' Space the poll (and every content op — title, state, pending, decide, import) requires an allow-listed Google sign-in before the server will decrypt-once and return anything — the room key alone no longer reveals the history. The server is the plaintext chokepoint (it decrypts in-use), so it enforces the allow-list on reads exactly as on writes (room-link-auth §4). Live + proven in prod: a token-less poll to an email-gated Space returns 403. An open Space is unchanged — the link reads.

The reply arrives progressively

A turn does not wait for the model to finish. As the reply is generated it is sealed and published as a partial (agent/asyncTurn.mjsputPartial), and that sealed partial rides every poll response — including the etag-unchanged short-circuit, so it is delivered without disturbing the content-blind fast path. The client opens it with mk and reveals it at a readable rate.

The value is not a shorter wait. It is that the reader starts processing the answer sooner. The partial is cleared the moment the finished entry lands in the ledger, so a live bubble can never duplicate a real message — including on the failure path, where a stream that died after emitting text must also disappear.

5. The ops

One HTTP surface (agent/spaceHandler.mjsagent/spaceService.mjs), at /space:

op what it does
create register the public Space record (its key commitment + config + gk); the client mints room+mk, the server never sees mk
turn a member posts; the render replies (or proposes a high-stakes tool call — see delegated authority)
poll the default read — a content-blind etag over ciphertext detects changes, then the server returns a decrypt-once plaintext delta (plus any sealed partial); on an email-gated Space it requires an allow-listed sign-in
pollsealed the same history returned still sealed — the server takes no key and decrypts nothing; the client opens the envelope itself
decide approve/deny a pending proposal (delegated authority)
pending list pending proposals
import seed the Space from a public ChatGPT share link (SSRF-guarded) or a WhatsApp export
title / state a sealed room title; the shared co-edited widget doc
blob sealed out-of-line content (images, files). Content-addressed and immutable, so a client may cache a ref forever; the server never opens it
subscribe / unsubscribe Web Push registration for away members (content-free tickle)
view a scoped read for an admitted agent: decrypt with a present member's mk, keep only what the agent's caps.perceive allow, and re-seal that slice to the agent's box key. The agent never receives mk
fetchview the agent collects that sealed slice. Un-gated and content-blind — it is ciphertext only the agent's box private key opens
mirror append a linked room's entries to this ledger (the private-lane bridge). Deduped by id, no agent turn. The server never joins the two rooms — the device does, holding both keys
policy resolve a room's governance + owner key, so a client can verify the room against its own pinned owner before trusting it
attest the running service reports the source it was built from (verifiable trust); cross-check against the signed build certificate
invite public, no mk: a joining partner fetches the commitment + the sealed share, unseals it with the invite code and combines it with their own share to reconstruct mk. The server only ever hands back ciphertext

6. What runs inside

A Space's capabilities are ordinary tools in its config (e.g. search_places → a shared map widget, search_flights → a flights widget). The turn runs the normal brain tool-loop over them. The one async-specific twist: a high-stakes tool call is deferred to a human approval instead of running in-room — that is delegated authority.

7. Code map

Concern Where
Turn render (decrypt-once) agent/asyncTurn.mjs (runAsyncTurn, renderLedger)
Access control + mk lifecycle agent/spaceTransport.mjs, agent/spaceService.mjs (handleSpaceRequest)
Content-blind poll agent/spacePoll.mjs (etagOf, pollSpaceView)
Sealed ledger agent/sessionStore.mjs, agent/envelope.mjs (seal/open/commit/newRoomKey)
HTTP handler + record store agent/spaceHandler.mjs
Thin client spaces/public/spaceClient.js, spaces/public/space.html
Delegated authority agent/delegatedAuthority.mjs (doc)

8. Status

Live in production: the /space zip Lambda (agent brain as a function) + the Spaces app at witbitz-spaces.pages.dev.

Shipped: the turn core; transport (mk-commit); content-blind poll; delegated authority; per-member signed identity; progressive (streamed) replies; Web Push for away members; the 2-of-2 code-sealed couple handshake (spaces/public/invite.js — the server only ever holds ciphertext under a code it does not have); private lanes; membership forks; email-gated admission; import from ChatGPT and WhatsApp; the built-in tool catalogue.

Ledger v3 — appending only the new entries as per-entry sealed boxes, so a turn's crypto is O(1) in the delta rather than O(n) in the whole history — is enabled in production (SPACE_LEDGER_V3=1) while the code default stays off, so a rollback is one environment variable. v2 seals the whole blob each turn and remains the fallback.

Deferred: delivering an agent's scoped view while no member is present (a push-on-turn sealed-view store); compaction of very long ledgers beyond the current bound.


Trust model

Witbitz makes two separate trust claims. Keep them separate:

  1. Privacy is verifiable — the double blind. A Space is sealed to a key the platform never holds; the app has no backend of its own; the browser egress footprint is locked; and the deployed render can be rebuilt from published source and checked against a signed certificate. Neither the operator nor the app's own owner can read the users' data — and that's a property you check, not a promise.
  2. Humans keep authority — delegated authority. An AI agent can propose work, but the runtime requires a human approval before any high-stakes action executes. No action runs without an approval.

This page states both guarantees in full. It is intentionally explicit about what is live today and what is not — start with Status if you need the shipped / preview / designed map, and Verify it yourself to run the checks in your own terminal.


What is protected today

Claim Production status
A gated Space refuses reads and writes without an allow-listed identity Live and checkable
The stored Space ledger is ciphertext sealed to the room key Live and checkable
There is no operator recovery recipient in the production envelope Live and checkable
The app's browser egress is locked to a published allowlist Live and checkable
The deployed render publishes a signed build certificate Live and checkable
The render source can be rebuilt to the deployed hash Live and checkable
High-stakes agent actions require approval Live and tested

Run the checks: Verify it yourself.

What is not hidden

Witbitz does not hide everything, and the docs should never imply that it does.

  • Metadata remains visible. The platform sees that a room exists, when it changes, and roughly how large writes are.
  • The model provider sees turn content — on the frontier trade. A remote frontier model must receive the text it reasons over, so a room that keeps the frontier model (the default Space) exposes its turns to that vendor. The confidential/attested tier closes this: its model call leaves the measured image only through an attested no-log gateway into confidential-compute inference on an open-weights model (fail-closed if that gateway does not attest) — see the attested tier.
  • The render sees plaintext in use today. Production Spaces open plaintext inside the server-side render during a member's active read or turn.
  • Endpoints still matter. A compromised member device can read what that member can read.
  • Availability is not privacy. The operator can refuse service. The content-blind claim is about reading, not guaranteeing access forever.

Guarantee 1 — The double blind

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. What holds today, exactly where it stops, and how the remaining "readable in use" gap is closed by the attested tier, now in production.

Sealed to a key we never hold → 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)). To the operator, a Space is opaque bytes — storage and transport are content-blind, because the platform holds no key. There is no operator recovery key: every record is sealed to the room key alone, so there is nothing on the platform that can read it. The double blind is the only configuration that runs in production.

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). 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 when no one is.

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 the attested tier exists (below). Keeping it one small, auditable path is exactly what makes attestation tractable.

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.

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; that gap is closed cryptographically only in principle (a never-receives-mk, append-only tier whose sealed client→actor channel is unbuilt and not planned) — the shipped way to close it is the attested tier.

Status — what holds today

Property Status
Sealed ledger, platform holds no key (nothing on the platform can read it) shipped
No operator recovery key (sealed to the room key alone) shipped & CHECKABLEPOST {"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)
Attested tier (operator provably blind in use) Production — verifiable on real Nitro hardware, scale-to-zero; the app builder enables the enclave per room (attested tier)
Attested client (a hardware-attested native shell carrying the audited bundle) Design — native shells not shipped; the deployed web app already verifies the enclave's attestation, which is part of the attested tier above (verified client)

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 in production — verifiable on real Nitro hardware, launch-on-demand and scale-to-zero. Four checks run in a stranger's terminal today, and the fifth is in production for any Space the app builder runs in the enclave. What remains genuinely unbuilt is the attested client in native shells.

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) the published render source

The double blind is not "trust us less." It is "you don't have to" — sealed to your key by default, and provably unobservable in use for any Space that turns on the attested tier.


Guarantee 2 — Delegated authority

The second guarantee. An agent never owns authority — it receives it, limited and revocable, and a human approves anything high-stakes before it happens. The runtime enforces one invariant: no action runs without an approval.

Built + tested (async Spaces M2 Cut 4); runs inside the deployed /space render. Code: agent/delegatedAuthority.mjs.

Propose → approve → execute

An effectful action moves through three recorded steps, asynchronously (the human may approve on a later turn, not in-the-moment):

  1. propose — on its turn, the agent proposes an action; it is recorded pending, not run (propose(room, { action, by })).
  2. approve / deny — a human decides on a later turn (decide(room, { id, approve, by })). A decision on a non-pending proposal is refused; a decided proposal can't be flipped.
  3. execute — only an approved proposal runs, through the injected executor (execute(room, { id, run })), and only once.

The invariant

Enforced and unit-tested: an un-approved proposal never runs, and no proposal double-runs. Approval is a precondition to execution, checked in the runtime — not left to the model's discretion. The model reasons, the human authorizes, the runtime gates.

Capabilities are just tools

A Space's effectful capabilities are ordinary tools in its config (send_summary, schedule_checkin, …) — there is no separate "actions" concept. The turn runs the normal brain tool-loop; the only async-specific twist is that a high-stakes tool call is deferred to a human approval instead of running in-room. On approve, execute runs the same proposal through the same tool executor a live call would use, keyed on the tool name — so approval routes to the real implementation.

The audit trail

Every step — propose / approve / deny / execute (with result or error) — is appended to a single per-room authority doc, an append-only log. It is mk-sealed like the ledger, so it is content-blind (the platform can't read it) and it persists across weeks. A wrong mk yields nothing — the trail is readable only to a present member, in the decrypt-once render.

Richer policy — spend caps, per-capability authority tiers, org-level delegation — builds on this primitive. The one-method-call SDK surface for it is Delegated authority in the SDK.


Identity and admission

A Space starts with key possession: holding the room key proves membership. Gated Spaces add signed identity on top — Google/OIDC identity, signed invites, agent keys, owner policies. The link is the admission capability; the owner decides who may join and at what tier; and members join, rotate devices, and are revoked over a Space's life. The full mechanism — identity, the verified room, admission tiers, the owner rule, member lifecycle, and recovery — is one doc: Identity and admission.

Stronger tiers

Tier Status What it changes
Owner-governed rooms Implemented The app owner can mandate admission tiers and capabilities; built and tested, not yet enabled in production (owner rule).
Attested server tier Production A Space's turns run inside measured Nitro hardware, key sealed to the image; launches on demand, scales to zero when idle. The platform can run any room in it; enabling it is the app builder's choice (attested tier).
Attested client Design (native shells) The deployed web app already verifies the enclave's attestation and pins its measurement before sealing your key — that half is live, part of the attested tier; a native binary carrying the audited bundle is still future work (verified client).

For the full mechanism-by-mechanism breakdown — the canonical maximum, the orthogonal privacy axes, the named modes, and the feature × mode matrix — see the privacy spine: Privacy tiers.

Where to go next


Privacy tiers — the canonical configuration, and how modes trade down

This is the spine for the privacy story. Every other doc describes a feature; this one describes the maximally-private configuration once, then defines each real-world mode as a delta from it — what it gains and, precisely, what it leaks and to whom. Read this first; the feature docs hang off it.

Honesty rule (read before quoting anything here). This page describes the ceiling — the maximal configuration, including mechanisms that are still Design. What is actually in production today is in the Status table and what you can check yourself is in Verify it yourself. Every mechanism below carries its own maturity (Production / Implemented / Design) so the ideal is never mistaken for the real.


1. The always-on foundation (every Space, live, not optional)

These are not a "tier" — they are the floor beneath every Space, live and checkable today:

  • Admission gate — a gated Space refuses reads and writes without an allow-listed identity, even with the room key.
  • Egress lock — the browser-visible app can reach only a published allowlist (connect-src); everything else is refused by the browser.
  • Content-blind storage — the store holds only ciphertext sealed to the room key; there is no operator recovery recipient.
  • Signed, reproducible build — each origin — the deployed render and the mk-holding vault/kernel — publishes a /cert.json + assets-manifest binding its exact deployed bytes to published source, which rebuilds to that hash. (Store origin + a CI rebuild-and-compare job still to come.) This is the keystone: it is what makes the claims above checkable rather than asserted.
  • Key confinement (vault sandwich) — room keys live in the L1 vault + Kernel worker; the app frame is keyless, enforced by Same-Origin Policy, not app discipline. See Client architecture.

Everything in §2 layers on top of this foundation.


2. The canonical maximum — the "Maximum" tier

The highest trust level is the foundation plus all seven of the mechanisms below, each of which closes one specific leak against one specific party. They are orthogonal — independently switchable, each shutting a different door — so the "levels" in §5 are bundles of these, not a single ladder.

code
Foundation (always on)
  + Server attestation   → operator/cloud can't read plaintext in use
  + Client attestation   → the running client is provably the genuine build
  + Client verification  → the locks are provably enforced in YOUR browser
  + OHTTP                → the server can't see WHO (your IP) is connecting
  + Client shaping       → a network observer can't read sizes / timing / patterns
  + No notification      → no push provider learns THAT / WHEN you got a message
  + Confidential model   → the model that answers the turn can't read it either
= Maximum

The platform ships this; apps configure it. The "Maximum" above is the platform's supported capability, not an aspiration — everything except the roadmap items (client attestation and client shaping) is shipped and available to every Space. A mode (§5) is a bundle of these mechanisms; a lower mode simply omits some. Which mode the first-party Spaces app ships at is a separate, app-level concern — see The Spaces app: default configuration.


3. What the platform ships today

Mechanism-level view — the platform's status for each. (What the first-party Spaces app enables by default is a separate concern: The Spaces app: default configuration. Full product map: Status. Client-side checks: Verify it yourself.)

Mechanism Platform maturity
Admission gate Production (structural — every Space)
Egress lock Production (structural — every Space)
Content-blind storage Production (structural — every Space)
Signed + reproducible build Production (structural — every Space)
Key confinement (vault sandwich) Production (structural — every Space)
Server attestation (Nitro enclave) Production — the app builder enables the enclave per room
Client verification (verifier extension / CSP-loader) Implemented — the self-run tool + the Windows verifier extension are built and proven on staging, not the prod default; the mobile companion is Design
Client attestation (hardware-rooted binary/device proof) Design — iOS App Attest / Android Keystore / Windows TPM verifiers are stubbed + fail-closed; native shells not shipped. (Client verification above is software release-attestation — a separate, more-mature thing.)
OHTTP (network-metadata) Production — oblivious transport (RFC 9458) is LIVE: the app routes room ops by postMessage to a pinned open-source L1.5 frame that verifies a client-attestable relay (GCP Confidential Space, relay.witbitz.chat) and encapsulates to the gateway. Sticky opt-in (?ohttp=1); every room data op goes through the relay. (verify)
Client shaping (traffic-analysis) Design
No notification Push is Production (off unless a user enables it — a configuration choice)
Confidential model (attested inference) Production — the confidential tier routes its model call through an attested no-log gateway into confidential-compute inference on an open-weights model, verified transitively from the render enclave's measurement (the attested tier).

Everything above is Production today except client attestation and client shaping, which are Design — the gap between what ships and the full ceiling. (OHTTP is Production, off by default behind the "Hide my IP" toggle.)


4. The seven mechanism axes (orthogonal)

Each axis: the leak it closes, the party it closes it against, and its platform state.

Axis Closes this leak Against whom Platform state
Server attestation plaintext readable during the render instant the operator + the cloud (AWS) Production (the app builder enables the enclave per room)
Client attestation (hardware) a participant's client is a tampered/forged build masquerading as the genuine app — a modified client that leaks, or a bot impersonating one whoever runs a forged/tampered client in your room (checked by the room's requireAttestation gate — hardware proof via App Attest / Keystore / TPM) Design — App Attest / Keystore / Windows-TPM verifiers stubbed, fail-closed
Client verification the JS actually running in your browser might not be the audited, egress-locked build — a leaky client could exfiltrate mk/plaintext whoever serves your client — the app builder itself, the operator, a CDN, or a network injector — a leaky or swapped build, on purpose or by mistake (verifying the client is what makes the double blind — even the app's own owner can't read your content — hold) Implemented — self-run tool + Windows extension proven on staging; mobile companion Design
OHTTP your IP / the fact you're connecting the server + the network path Production — a single attested relay (Confidential Space, verified client-side via the pinned L1.5 frame, RFC 9458) sees your IP but only ciphertext; the gateway decrypts the op but sees the relay's IP. LIVE for all app→AWS traffic: room data ops and the enclave attest/wake path (previously a direct-to-gateway probe) now both ride the relay when the toggle is on — fail-closed, so a relay hiccup never silently falls back to a direct IP-revealing POST. The relay is measured code that provably does not log the source address — verifiable, not a promise. Residual: the browser still reveals your IP to Cloudflare when it loads the app's own bytes and calls same-origin edge functions (/space-config, tiles, /api/appshare, /api/link) — Cloudflare already serves the app, so this is no new party, and AWS never sees you (CF proxies onward from its own IP). Hiding your IP from Cloudflare/Apple/Google (app-load, push, Google sign-in) is outside any web page's control — only a device-level VPN or Tor closes it, which composes cleanly with OHTTP.
Client shaping message sizes, timing, access patterns a passive network observer Design
No notification that/when you received a message (content-free tickle + timing) the push provider (Apple/Google/push service) Production
Confidential model the turn's plaintext, read by the model that answers the model vendor / host Production — the confidential tier's measured image routes only to an attested no-log gateway (Intel-TDX-verified in-image, fail-closed) into confidential-compute inference (Intel TDX + NVIDIA GPU, gateway-verified); trades frontier for open-weights (the attested tier)

Note — client verification comes in three forms: the self-run tool (on-demand; Verify it yourself §8), the Windows/desktop extension (continuous — pins the release + forces the CSP; Implemented), and a mobile native companion (Design). "Client-verify on" in §5 means the extension/companion (continuous) or a tool run (on-demand). This is software release-attestation — distinct from client attestation above, which proves the binary/device is genuine via hardware.

See: The attested tier, The attested client, The power-user stack, The verifier, Away notification.


5. Named modes = bundles of axes

Modes are named bundles of the §4 mechanisms — the platform supports the Maximum; a lower mode simply omits some. Names encode the trade, not a number. (Which mode the first-party Spaces app ships at is documented separately: The Spaces app: default configuration.) on, off, Design (not built yet).

Mode Server-attest Client-attest Client-verify OHTTP Shaping Confidential model Notifications In production today?
Maximum (full platform support, §2) off Partly — client shaping / hardware client-attest are Design (OHTTP is Production; on in this mode, opt-in in the app)
Attested (strongest shippable) off Yes
Verified off Yes
Baseline (foundation only) off Yes
Connected (convenience) on Yes

Governance overlays (regulator access, full audit trail, owner rule) are orthogonal to these privacy modes — a compliance opening is a named-recipient disclosure, not a privacy-mode step-down. Keep them on a separate axis; see Delegated authority, The owner rule. Air-gapped / on-prem deployments compose on top of any mode — see Air-gap, On-prem.


6. Feature × mode matrix

Cells: available and privacy-neutral · ✓·leak(party) available but discloses something specific · unavailable at this mode. Only rows that actually trade are shown — every other feature (posting turns, membership, recovery, offline export) is mode-invariant and simply .

Feature Maximum Attested Verified Baseline Connected
Async chat, private lanes, documents, widgets, artifacts
Local connectors (on-device: folder / Obsidian / local MCP)
Away notifications (push) ✓·leak(push provider: that/when) ✓·leak(push provider) ✓·leak(push provider)
Web search / flights / places (egress tools) ✓·leak(the named API) ✓·leak(the named API) ✓·leak(the named API) ✓·leak(the named API)
Cloud connectors (Google Drive/Docs, etc.) ✓·leak(the cloud: content synced) ✓·leak(the cloud) ✓·leak(the cloud)
Live call / voice ✓·leak(media path) ✓·leak(media path) ✓·leak(media path)
External model answers the turn ✓·attested route † ✓·leak(model vendor) † ✓·leak(model vendor) † ✓·leak(model vendor) † ✓·leak(model vendor) †

The model residual. If an external model answers, the model vendor receives plaintext to compute — attestation covers the render, not the vendor. The Confidential model axis (§3–§4) closes this and is Production on the confidential (attested) tier: the render enclave's measured image routes the turn only to an attested no-log gateway into confidential-compute inference on an open-weights model, each link verifying the next (verification is transitive from the render measurement; the weights themselves are vouched by the attested stack, not measured — see the attested tier). Modes that use a frontier model still leak the turn to that vendor — that is the frontier-vs-confidential trade, chosen per room. See The double blind.


7. Opting out — what each step gains and what it leaks

Each step omits one platform-supported protection — an explicit, named trade. Read top-to-bottom as "relax one thing, gain one thing, leak one named thing to one named party."

  • Maximum → Attested. Gain: the egress tools (web search / flights / places) — real external capability. Leak: each tool call's content to that named API; and OHTTP/shaping being off means the network path sees your IP, sizes, and timing. Party: the specific tool API + a network observer.
  • Attested → Verified. Gain: runs on any deployment (no enclave required), lower cost. Leak: the operator + AWS can read plaintext during the render instant (server attestation off). Party: the operator + cloud. Client integrity is still verified.
  • Verified → Baseline. Gain: no verifier step, frictionless. Leak: you're back to trusting the server's declaration that the locks hold, rather than confirming them; cloud connectors become available (content to the cloud). Party: yourself (trust restored) + any cloud you connect.
  • Baseline → Connected. Gain: away notifications (and convenience integrations). Leak: the push provider learns that and when you received a message (content-free, but the tickle + timing). Party: Apple/Google/the push service.

8. Residual limits (the honest floor, at every mode)

No mode removes these; state them plainly so the tiers never over-reach:

  • Model vendor (frontier rooms only) — a room that chooses a frontier model hands that vendor the turn's plaintext (§6 †). The Confidential model axis (§3–§4) that removes this is Production: on the confidential tier the LLM runs as confidential-compute inference (Intel TDX + NVIDIA confidential GPU) behind an attested no-log gateway the render enclave verifies in its measured image, trading a frontier model for an open-weights one (the attested tier). The residual stands only where the frontier trade is chosen.
  • The endpoint / analog hole — plaintext must exist on a device for a human to use it; a compromised endpoint (keylogger, screenshot, a connector the client doesn't know about) is outside every software guarantee. Managed devices / air-gap raise this bar; software alone cannot. See Air-gap.
  • Metadata — IP hidden, shape not yet. Your IP is hidden by OHTTP ("Hide my IP", Production) — no party sees both your IP and your rooms. Message sizes, timing, and access patterns remain visible to the network path until client shaping ships.
  • Revocation offline — attestation/verification chains verify offline against pinned roots; revocation freshness needs connectivity.
  • Availability is not privacy — the operator can refuse service; content-blindness is a claim about reading, not about guaranteeing access forever.

How this reorganizes the doc set

This page is the entry point for privacy claims. The existing docs slot under it:

Update rule: when a mechanism changes state, update §3 and Status together; when a feature's disclosure changes, update its §6 row.


The Spaces app — default configuration

This documents the out-of-the-box configuration of the first-party Spaces app — which of the platform's privacy mechanisms it turns on by default.

It is one app's configuration, not a platform property. The platform supports the full stack (see Privacy tiers); this page records only what Spaces, as shipped, enables. Any other app built on the runtime makes its own choices.


What the Spaces app enables by default

Mechanisms are defined in Privacy tiers; this table is only the first-party app's setting for each.

Mechanism Spaces app default Turn it on by
Foundation — admission gate, egress-lock, content-blind storage, signed + reproducible build, key confinement On — structural, cannot be disabled
Server attestation (Nitro enclave) Off "Run this Space inside the enclave" — opt-in per Space
Client verification (external verifier) Off Install the Windows verifier extension, or run the verifier tool
Client attestation (hardware) Off — not shipped — (Design)
OHTTP (network-metadata) Off by default — shipped "Hide my IP" toggle (sticky, per device). Routes all app→AWS traffic through a pinned L1.5 frame + an attested relay (GCP Confidential Space) so no party sees both your IP and your rooms — see Privacy tiers
Client shaping Off — not shipped — (Design)
Notifications (content-free push) Off Enable push on an installed PWA — opt-in per install

In mode terms

So the first-party Spaces app ships at the Baseline mode (Privacy tiers §5) — the always-on foundation, with the enclave, the verifier, and push each available opt-in per Space or per device. Turning any of them on moves that Space up the ladder (Verified → Attested → …), with the trades named in Privacy tiers §7.

The foundation is not a choice — it is structural and holds for every Space regardless of these settings. What this page records is only which of the optional mechanisms are on by default, and Spaces leaves them off so the app stays frictionless while the stronger tiers remain one opt-in away.

Update rule: when the Spaces app changes which mechanisms it enables by default, update this page only — the platform's capabilities live in Privacy tiers and Status and are unaffected.


Identity and admission

Identity in Witbitz is not "there is an account in our database." A Space is self-authenticated: its link carries the public material needed to verify who belongs, and members hold the corresponding private credentials. The same definition works with no server at all (true peer-to-peer) and with a server that verifies but cannot read. It is inherited from an open, proven real-time engine — where it secures live P2P calls — and carried, mechanism for mechanism, to asynchronous, end-to-end-encrypted Spaces.

This page is the whole subject: the quick reader model, then the definition and its primitive, the credential families, how membership is proved, the owner rule (who decides admission and at what tier), and — the part usually skipped — removal, rotation and recovery, marked precisely shipped vs designed. For a privacy product, overclaiming the lifecycle is worse than admitting a gap.

See also: the double blind (why the platform can't read the room) and Verify it yourself (reproduce the gate in your terminal).


The quick model

Base membership: hold the room key. Every Space has a room key mk. The client mints it and stores only a commitment on the server — commit(mk) = base64url(sha256(mk)). When a member acts, the render checks the presented key against the stored commitment: a right key proves the caller holds the link; a wrong key opens nothing. This proves a member is present — not which member.

Per-member identity: signed entries. To attribute entries to a specific person, each member holds a signing key. The room's public invite key verifies the member grant; the member's public signing key verifies the entry. Because the author is inside the signed core, a rename or forged attribution fails verification.

Admission families. A room composes any of these verifiers (e.g. a human gate and an agent allow-list):

Family What it proves
Key possession The caller holds the Space key.
Signed invite A creator or room authority granted this member access.
OIDC / Google The member authenticated as an allow-listed identity.
Agent key This agent key is allowed by the room.
Capability grant This participant may perceive or act in specific ways.
Owner policy The app owner mandated admission tiers for the room (the owner rule).

Gated Spaces. On an email-gated Space the link alone is not enough: reads and writes both require an allow-listed sign-in — which is why the verification page starts with token-less poll/turn requests that return 403. The allow-list lives in the sealed config, so the platform never reads the membership list.

Live vs designed, at a glance:

Piece Status
Key-possession membership Shipped
Solo signed identity Live and verified
Email-gated reads and writes Live and checkable
Agent-key admission · scoped agent reads Deployed
2-of-2 per-member signed path · distinct invite links Not yet
Crypto-strong single-Space read revocation by epoch re-key Implemented (fork-based re-key; built, wired, tested)

The rest of this page is the mechanism behind that summary.


The definition

A verified room has no account server and no stored access-control list. Instead:

The link carries a verifier, never a secret. A member holds a credential. Anyone can check credential-against-verifier using only the link's public material — so trust is rooted in the link, not in any server.

Three consequences fall out, and together they are the whole model:

  1. The link is a capability. Public verifier material — a room-invite public key, a creator public key, a name list, an OAuth client id — rides the link openly. Secrets — the room key, a member's private signing key — ride the URL fragment, which a browser never transmits. Witbitz adds one thing to the live-call link: the fragment also carries the decryption key for the room's content, so a Witbitz link literally holds everything.
  2. Verification is cryptographic, not a lookup. "Is this a member?" is answered by verifying a signature against a public key from the link — never by asking a database who someone is.
  3. The verifier can be anyone. Because the check needs only public material, the party that runs it is a free choice — a peer, a reader, or a certified function — without changing the credential. This is the hinge the rest turns on.

The hard constraint. Nothing sensitive is persisted server-side. After a Space is created and its member links handed out, the link is the entire authority: it reconstructs the key, proves membership, and (optionally) signs entries. The server keeps only a commitment to the key and the sealed ledger — never the key, never plaintext.

Membership = key possession (mkCommit)

The base Space auth is possession of the room key mk, minted client-side and living in the link fragment — only its commitment is ever stored (commit(mk) = base64url(sha256(mk)), agent/envelope.mjs). On every mutating op the render checks commit(presented mk) === the Space's stored mkCommit:

  • a right key → you hold the link → you are a member → the turn runs;
  • a wrong key → 401 unauthorized, sealed ledger untouched (a wrong key also decrypts nothing);
  • first use (no commitment yet) → the server returns commitOut so the deployment persists it, pinning the key model on first turn.

The key model (k-of-n) governs presence, not per-member identity: mk can be split so presence is required to reconstruct it. 1-of-1 (solo)mk rides one link fragment (#mk=…). 2-of-2 (couple) — a Shamir split where each partner's link carries one share (#s=…) and mk exists only when both combine; the creator seals shareA under an out-of-band code and registers it as the room's invite, the partner unseals with the code and combines.

The primitive

Every credential in the model is the same ECDSA P-256 token, and it is tiny (the live-call engine's invite token, reimplemented byte-for-byte in agent/spaceEntrySig.mjs):

code
token = base64url(JSON.stringify(payload)) + "." + base64url( ECDSA-P256-SHA256 sig over the first part )
  • Bearer + unforgeable. Minting needs the private key; verifying needs only the public key. Forging one is forging ECDSA, not guessing a short code.
  • Room-bound + expiring. Every payload carries room and exp, so a token cannot be replayed into another room or used forever.
  • The link holds only the public half, so any authority — even one that took over after the creator left — can verify without ever holding a private key.

signPayload / verifyPayload are the shared floor; invite grants, agent assertions and host commands are all this token with a different payload and a wire tag, so they can never be confused for one another.

The credential families

A room can carry any of these verifiers; it composes them.

  • Signed invites (inviteToken.ts). The creator holds an invite keypair; each guest gets a grant {name, room, exp}; the link carries the invite public key (gk). A signed invite link, cryptographically — no server, no account.
  • The signed manifest (roomManifest.ts). The committed roster {room, mode, exp, members?, domains?, agentKeys?}, signed by the creator key and verified against the creator public key (gm) in the link, so every peer checks a joiner against the same committed roster. Can be passphrase-encrypted so a mere link-holder can't read who is allowed.
  • Join-gate modes (joinGate.ts). open · names · code (mailed secret, constant-time compared) · email/google (OIDC) · invite. All but email are serverless.
  • OIDC + cert-binding (oidcVerify.ts · oidcBinding.ts · identityCert.ts). "Sign in with Google," without a backend and without trusting the transport. oidcVerify is hand-rolled RS256-only ID-token verification — alg:"none", HS* (algorithm-confusion) and ES* are rejected outright, the algorithm never taken from the token; the key is chosen by kid from the provider's JWKS, unknown kid fails closed. oidcBinding is the serverless heart: at sign-in the user sets the token's nonce to a hash of their WebRTC DTLS cert fingerprint, and every peer recomputes it from the cert it actually handshook with — so a token is valid only for the peer holding that cert's private key. identityCert pins one cert, private key never leaving the browser → non-transferable. Cert-binding composes with the emoji safety code (both read the same cert), so "this is really Emma" and "there is no man-in-the-middle" collapse into one guarantee.
  • Agent keys (agentKey.ts). An agent's identity is an ECDSA keypair; to enter it signs a cert-bound assertion {k:"kbz-agent-key.v1", room, fp, iat}, admitted iff the key is on the room's committed agentKeys and the assertion is bound to this connection and fresh. The room stores only the public key.
  • Capabilities (capabilities.ts). A per-participant Grant = what it may perceive (read-chat, read-roster, read-media, see-screen, hear-audio, receive-directed) and act (send-chat, speak, act). defaultGrant('agent') is least-privilege — an agent perceives the conversation but acts on nothing until granted.
  • Verified host (hostKey.ts). Admin is bound to a password, not to who holds the room id: the link commits the host public key (gh) and the private key sealed under a host password (ghk). It survives a coordinator migration — every peer already holds the committed key from its own link.

Per-member identity in an async Space

Status — per-participant verified identity is LIVE (deployed + enforcement-proven in prod). Two admission paths sign every turn and the deployed /space render verifies them, attributing each entry to a verified author (forged / unsigned / non-allowed → 400 bad_attribution / 403 not_authorized). Reads are gated too: every content op requires an allow-listed sign-in, so the link alone reveals nothing. Remaining: the couple (2-of-2) path is still unsigned, and distinct per-member invite links aren't built.

A live call attributes each message via the DTLS-bound connection; an async Space has no live connection, so each ledger entry carries its own signature. A member holds an ECDSA signing keypair; their grant is the live-call invite token — byte-identical to inviteToken.signPayload — with the superset payload { name, room, exp, spk } where spk is the member's signing public key, signed by the room invite key and verified against the public gk. Each entry is signed over a canonical core { room, author, ts, kind, id, text }author is inside the signed core, so a rename is unforgeable. The email/Google path additionally binds a fresh, externally-issued OIDC identity to the signing key (nonce = spkNonce(spk)), so an entry proves "signed by the keyholder who authenticated as this account, now" — a leaked link is then no longer enough to post.

One definition, three verifiers

The verifier can be anyone — which is what lets the same credential model span three runtimes. Only who runs the check and what fresh value the credential is bound to change:

Live P2P call Async P2P read Async server verify
Who verifies a peer — no server any reader, at read time the certified-ephemeral function
Membership the join gate over presence hold the key (commit(mk) == mkCommit) same
Freshness / anti-replay the live DTLS cert fingerprint the per-entry signature a server-issued challenge, folded into the signed core
Trust root the link's public key the link's public key the link's public key + a certified deployment
Content secrecy peer-to-peer E2EE sealed ledger, content-blind platform + owner same — the double blind

Async did not need a weaker model. It needed a different anchor for the same model — the member's signing key (identity) plus a server-issued challenge (presence) in place of the DTLS fingerprint.

Where the server sits, precisely. In async there is no peer to run the gate, so the turn function does — but it is a certified, ephemeral, content-blind verifier, not a trusted ACL store: it checks signatures against the link's public keys and a public JWKS (rooting no trust of its own); the allow-list it consults is sealed under mk, decrypted for one turn; an OIDC email it verifies is transient, never persisted; reads it serves an agent are sealed to that agent's key. The server verifies without being trusted with content.


The owner rule

Status: Implemented — built and tested, behavior-neutral, not yet enabled in production. The full path is implemented and tested end-to-end for both trust-root modes: on-prem (the owner pins one OWNER_POLICY_KEY) and hosted multi-tenant (a tenant registry, OWNER_REGISTRY=1, resolves the trusted key by the room's app). Also built: the create-time mandate, the render's integrity gate, tier-driven capabilities, client peer verification, tenant self-registration (op:'register-owner'), and key rotation/revoke (op:'rotate-owner'). It stays inert until an owner opts in — keys unset, rooms behave exactly as before.

A Witbitz room is a door whose key is the link — great between equals, a problem the moment there's an owner. If a company runs an app on Witbitz, it has no way to say "every room in my app is only for my people, and gold customers get more than free ones." The owner rule turns "the link is the authority" into "the owner is the authority," opt-in, without giving up content-blindness.

The rule: a signed App Policy. The owner writes one small document and signs it with a key only they hold:

code
AppPolicy {
  app:      "acme-portal"                       // which app this governs
  ownerKey: <owner's public key>                // the root of authority
  tiers: [
    { name:"open",   admit: anonymous,                                      caps:{ read },                       limits: low  }
    { name:"member", admit: { issuer:"acme.okta.com", any:true },           caps:{ read, chat },                 limits: mid  }
    { name:"gold",   admit: { issuer:"acme.okta.com", claim:{group:"gold"} },caps:{ read, chat, act, premium },  limits: high }
  ]
  roomTiers: { default:"member", admits:["open","member","gold"] }
}
→ signed by ownerKey

The signature is the whole trick: nobody without ownerKey can produce a valid policy, so nobody can forge a weaker one. Admission stops being binary — it's a ladder from anon-public to premium-verified, and the flat "must be my user" rule is just its one-tier case.

Bound to a room at birth. When a room is created through the owner's credential (hosted, the owner's tenant key; on-prem, its own create endpoint), create stamps the signed policy sealed into the room config (the render reads and enforces it) and a cleartext marker { app, ownerKeyId, policyHash } on the public record (so the content-blind platform and keyless read paths know "this room is owner-gated" without reading anything). The owner's create path refuses to mint a room that doesn't carry the policy — the room is born gated.

Only the app can mint its rooms (room-genesis). The signed policy is a reusable public artifact — anyone could copy it and stamp it on a room they create, standing up a look-alike to phish the owner's users. So stamping a policy at creation also requires a room-auth: a signature by the owner key over this specific room (the room id is in the signed core). Only the app owner holds the key, so only the app can mint "a genuine room of X's."

Tiers can't self-promote. A member's tier comes only from a signed source: IdP claims (the owner's directory is the source of truth for who is gold), a signed capability grant (a room admin promoting a member), and the room's own tier (the ceiling). A tampered client can't forge the claim or the grant, and the render — not the client — resolves and enforces the tier.

Enforced in three independent places, no single point to bypass:

When Check Where it runs
Birth create refuses any owner-room without the signed policy + marker the owner's create path (tenant-keyed / on-prem)
Writes (every turn) the render opens the sealed policy, verifies its signature against the pinned ownerKey, resolves the tier, gates each action; policy absent/invalid → refuse the certified render
Blind reads the cleartext marker makes keyless reads refuse; keyed reads pass the same identity gate the content-blind platform
Join the member's app verifies the room policy against its own pinned ownerKey and won't join an unrecognized room every member's app (peer check)

Per-tier limits (rate / token / tool budget) reuse the platform's existing metering — so the ladder doubles as the owner's pricing surface: admission, capability, and monetization collapse into one signed table.

Registration — Witbitz federates; it doesn't run the directory. A "registered user" is a record in the owner's identity provider, not a Witbitz account. Owner registration happens once (the trust root: ownerKey, the IdP issuer + JWKS as a SPACE_OIDC_ISSUERS entry, and the claim → tier map); user registration happens in the owner's own directory (enterprise SSO / SCIM / invite — the tier is a group the owner assigns, so a user can't self-register as gold); device enrollment mints a signing key per device bound at sign-in. So joining a room is authentication, not registration, and revocation is symmetric — remove the user from the gold group and their next sign-in drops a tier, enforced on the very next turn.

Orthogonal to attestation. This is the identity + capability axis; whether the client is trustworthy code is a separate axis (the verified client). They compose — a tier's admit can additionally require an attested client — but identity-tiers work on the plain web with no attestation in sight.

The honest trade. Requiring registration deliberately reintroduces a central authority — the owner's directory — into an otherwise account-free, link-based model. That's not a regression; it's exactly the control an owner asks for, and it's opt-in per app/room. The platform stays content-blind throughout — the policy is enforced in the certified render and by peers, never by a platform that reads the room.


Lifecycle — removal, rotation, recovery

The hard questions are never the initial key exchange. A bearer link is easy to copy, screenshot, sync, log, lose or leak — so these are the ones that matter.

After a member is removed

  • Writes — cut now (shipped). A revoked member is on the Space's revoked list (sealed in the config); the turn op rejects their next turn 403 even with a valid credential.
  • Reads — server cut shipped for email-gated Spaces; the crypto cut is designed. Every read op checks the allow-list
  • revoked before the server decrypts, so a removed member's next read is 403 — immediately, no re-key. Open Spaces have no read gate — the link reads. The crypto-strong version (holding even if the server were bypassed) is an epoch re-key sealing new content to a fresh key for the remaining set — live in the Bridge, not yet ported to the single-Space ledger.

"But the link is the auth — how can you cut anyone?" A link is a bearer credential: the removed member already extracted mk the first time they opened it, and no edit reaches into their browser to delete it. You cannot retract a held key — only abandon it. Revocation works because of the thin-client model: the client never decrypts — the server does. The server is a decryption oracle, and cutting someone means denying the oracle, not un-giving the key. (There's no "their slice" to strip — everyone holds the same mk; you remove them from the policy, not the key.) Dropping the member from the allow-list is a complete, shipped revocation for an ordinary private Space; an epoch re-key buys exactly one more thing — the cut holding without trusting the platform.

History vs future

Revocation cannot make someone un-see content they already read — true for every end-to-end system. A re-key protects the future, not the past. History a member already saw remains known to them; new content can be blocked by read gates now and by epoch re-key in the stronger model. For rooms with hard compartment boundaries, design epochs up front.

Membership change is a fork

Adding or removing a member is one epoch operation, not an edit to a member list. The room forks: a fresh room key and roster, the history carried across, and a sealed pointer left behind so present members follow automatically. The removed member keeps whatever they already had (that cannot be undone) but holds no key to anything after the fork, and no deny-notice has to reach every peer for the guarantee to hold. That delivery problem is what sank the earlier in-place "config update" and lease designs. Both the Bridge and single Spaces use epoch re-key on every membership change — single Spaces via spaces/public/spaceFork.js (built, wired to the Members UI, and tested in agent/spaceFork.test.mjs). Re-key is triggered by a membership change; absent one, a Space keeps a long-lived key (no scheduled rotation).

Rotation and replacing a compromised identity

Rotation = open a new epoch, re-seal forward, hand the remaining members the new key. Per-member signing keys rotate independently: revoke the old grant, admit the new key. Replacing a compromised identity needs no rebuild — a member is a signing key bound by a grant, not the room itself: revoke the old grant (shipped), admit a fresh key, and re-key the epoch where a crypto-strong future read-cut is required (designed). The Space, its history and every other member are untouched. Same for an agent — its identity is its key, so a rotated key is a one-line allow-list change.

Metadata that remains

Honestly: timing, sizes and coarse structure — never content or membership. The platform stores the mk-commitment, the sealed config and the sealed ledger, and observes when turns and polls happen and how big the ciphertext is. It does not see the conversation, the members' names (sealed in the config), or — in invite mode — any real-world identity. In Google-sign-in mode it sees the presenter's email transiently, at verify time, and discards it. The irreducible leak is timing correlation, which no content encryption removes.


Recovery

Witbitz deliberately has no operator recovery key. If the platform could recover a Space, the platform could read a Space. Recovery must belong to the user or organization, not to Witbitz as operator.

The current rule. If every copy of the room key is lost, the Space is lost. That is not a product flourish; it is the cost of the privacy claim — the production envelope is sealed to the room key, not to an operator, support, escrow, or beta recipient. Verify it with the sealed-store check: POST {"op":"sealed","room":"<ROOM>"} → the recipients list should name the room, not the operator (Verify it yourself).

Recovery must be user-held. Passkey (shipped) · backup code (shipped) · k-of-n social recovery (the same Shamir split the couple key uses — not built) · organization key policy (the customer controls it — on-prem). None require Witbitz to hold a readable recovery recipient.

The shipped vault. One vault per account, and either enrolled method opens it: a random 32-byte master secret identifies and encrypts the vault; each method stores one wrapper (the master secret sealed to that method's own secret); restoring with any enrolled method opens its wrapper, recovers the same master secret, and opens the one backup. So a passkey and a backup code are not two backups to keep in sync — they are two doors into the same room. The server stays content-blind throughout: it stores opaque blobs at derived ids and never receives the code, the passkey secret, the vault key, or any plaintext. Wrappers are written before the data they unlock, so a half-finished enrolment can never leave a vault nobody can open.

The offline export. Recovery that depends on the platform being reachable is not self-sovereign, so a Space can also be exported to a file you keep, alongside a standalone decryptor page that opens it with no server at all. The export is opaque by construction: an earlier version wrote each Space's room, title and message count in the clear next to its sealed ledger — unreadable ciphertext that nonetheless announced every conversation's name and size to anyone who opened it in a text editor. That metadata is now sealed under the same room key as the ledger it describes; the decryptor shows "Conversation N" until you load your keys file. Two things remain visible: how many conversations the bundle holds and when it was exported (hiding the count would mean padding with decoys — more cost than it buys for a personal backup).

Enterprise / on-prem recovery. In an on-prem deployment the organization controls the runtime boundary — its own backup policy, IdP, secret storage, and recovery procedure. The privacy story changes from "trust Witbitz not to read" to "verify the image you run and control the boundary yourself" (Deployment options).

What not to promise. Witbitz cannot recover a lost Space on hosted production — by design. And don't hide the user burden: user-held recovery is safer than operator-held, but it still requires a recovery artifact, another device, a passkey sync provider, a social-recovery set, or an organizational process.


A room whose link holds the key must never have that key stored by the shortener, or the operator would hold it. So the shortener is blind-safe: POST /shorten refuses any target carrying key material (k/key/mk/sk/secret/seed in query or fragment); GET /r/{code} re-attaches the short link's own #fragment client-side. The shortener may store the room's base, never the key.

Security properties

  • Unforgeable — every credential is an ECDSA P-256 signature; minting needs a private key.
  • No cross-room, no replayroom + exp in every payload, plus a per-connection fingerprint (live) or a single-use, TTL-bounded server challenge (async).
  • No algorithm confusion — OIDC verification is RS256-only; alg:"none"/HS*/ES* rejected; the verifier algorithm is never taken from the token.
  • Fail-closed everywhere — a malformed grant, unknown kid, missing signature or empty allow-list → no access.
  • No server trust root for the credential — a compromised or curious platform cannot mint membership, only (in the async-write locus) decline to run.
  • Sealed policy — the policy is sealed under mk; verified identities are transient.

Status

Piece Live P2P Async
Key-possession membership (mkCommit) n/a — presence gate shipped
k-of-n key model (solo / 2-of-2 Shamir) n/a shipped
Signed-invite grants + per-entry signatures shipped deployed + live-verified (forged → 400)
Capabilities in the grant (perceive/act) shipped deployed
OIDC / Google admission, allow-listed shipped (cert-bound) LIVE — enforcement-proven (unsigned / non-allowed write → 403)
Presence proof / anti-replay shipped (cert-binding) deployed (server-issued challenge)
Agent admission by allow-list (AgentEntry) shipped deployed (same shape, live + async)
Scoped read (least-privilege perceive) shipped (mesh) deployed (view + member-absent fetchview)
Identity-gated reads (the link alone reveals nothing) admission gates presence LIVE — enforcement-proven (token-less poll403)
Owner rule (signed App Policy, tiers, room-genesis) n/a Implemented — not yet enabled in prod
Blind-safe /shorten n/a deployed
Remove a member — cut writes shipped shipped (gated Spaces)
Remove a member — cut reads (server read-gate) n/a shipped for email-gated Spaces
Remove a member — cut reads (epoch re-key, forward secrecy) n/a shipped in the Bridge; designed for the single-Space ledger
Rotate a compromised member/agent key shipped shipped (identity + revoke) + designed (re-key for full read-cut)
Device-loss recovery (user-held code / k-of-n social) n/a passkey + code shipped; social recovery designed — never operator-held
Per-member identity — couple (2-of-2); distinct invite links n/a not yet (solo signed path live)

Code map

Concern Live P2P Async
Signing primitive (payloadB64.sigB64) src/core/inviteToken.ts agent/spaceEntrySig.mjs (byte-compatible)
Signed invites / grants src/core/inviteToken.ts agent/spaceEntrySig.mjs + spaces/public/entrySig.js
Signed roster / policy src/core/roomManifest.ts the mk-sealed config (agent/spaceHandler.mjs, spaceService.mjs)
Join-gate modes src/core/joinGate.ts admission open/invite/email in agent/spaceService.mjs
OIDC verify + binding oidcVerify.ts · oidcBinding.ts · identityCert.ts agent/spaceOidc.mjs (RS256/JWKS + spkNonce)
Presence / anti-replay cert fingerprint agent/spaceChallenge.mjs
Agent admission src/core/agentKey.ts agent/spaceAgentKeys.mjs
Capabilities src/core/capabilities.ts agent/spaceCaps.mjs
Scoped read mesh perceive-gating (mesh.ts) agent/spaceScopedView.mjs (view/fetchview)
Verified host src/core/hostKey.ts agent/delegatedAuthority.mjs
Room key mint / commit / at-rest seal agent/envelope.mjs (newRoomKey, commit, seal/open)
Recipient sealing (ECDH box) agent/envelope.mjs (sealTo/openBox)
Owner policy (sign / verify / register / rotate) agent/spaceHandler.mjs, tools/owner-policy.mjs
Link codec + k-of-n spaces/public/spaceConfig.js, shamir.js, invite.js
Epoch re-key + forward secrecy agent/bridgeMembership.mjs
Blind-safe short links control-plane/src/shortlink.mjs

The through-line: a room's authority lives in its link, and the check that enforces it can run wherever the room happens to be — on a peer, in a reader, or inside a function that verifies but cannot read.

Don't take our word for it → Verify it yourself. A token-less read/write to an email-gated Space returns 403; curl -I the app to read its browser-enforced egress allowlist; POST {"op":"sealed"} to read the exact ciphertext the server stores — recipients:["room"] proves it's sealed to your key, not the operator's.


The verified client, in full

Everything a browser runs for a Space is delivered from an origin, and an origin can be compromised — a popped hosting account, a bad deploy, a poisoned cache. The verified client closes that gap on two orthogonal planes, and a serious client needs both:

  • Verification (integrity)what code runs. The browser refuses to run any byte that isn't the exact, platform-signed release.
  • Confinement (custody)who can touch the room key. Key custody lives in an outer frame the app cannot read, so the browser's Same-Origin Policy — not the app's own discipline — is the wall.

They compose: verification pins the loader so only approved code runs; confinement keeps the key out of that code's reach anyway. Verify the prisoner and keep the cage. From there this page climbs the ladder to the strongest tiers — a native attested client, the verifier extension that forces verification on the real bytes, the power-user stack that removes Witbitz from the trust base, and the certified app that makes a closed-source app machine-auditable.

Status. The core chain — pinned key → signed cert → manifest → loader → __vcImport/__vcFetch → bundle + workers + chunks + pinned assets — is built and proven on staging (preview.witbitz-spaces.pages.dev) when forced by the verifier extension / ?vc — the verified client, Implemented, not the default production boot. Its extension-free, origin-served counterpart — the pinned bundle, a self-bootstrapping loader + a signed client manifest that content-pins the shell's imported module closure — is Design, not built. The confinement plane (vault sandwich + Kernel) is live in production. Where a piece is design or device-only it says so.

The one problem it solves

script-src 'self' — the normal same-origin policy — trusts any script the origin serves. That is exactly the wrong thing against a compromised origin: the evil script is same-origin, so 'self' waves it through, and stripping the Subresource-Integrity attributes off the HTML disables the per-file checks. The verified client removes 'self' and replaces "trust the origin" with "trust one small loader whose hash is pinned out-of-band, and let it check everything else."


Part 1 — Verification: what code runs

The trust anchor and the chain

Trust bottoms out at a pinned platform public key (P-256), published in verify it yourself and compiled into the loader and the verifier — never fetched from the app. From there, one signature covers the whole frontend:

code
PINNED platform key  →  signed /cert.json (frontendSha256, ES256)  →  sha256(/assets-manifest.json)  →  every file's hash
  • /cert.json is a platform-signed commitment. Its frontendSha256 is the SHA-256 of the served /assets-manifest.json; the signature is verified against the pinned key, and the cert is rejected if it embeds a different key (no key-swap).
  • /assets-manifest.json is a content-addressed map { path → sha256 } of every served file. Because the cert signs its hash, the manifest — and every file hash in it — is trustworthy once the cert verifies.

A compromised origin can serve an evil bundle, but it cannot forge a cert that verifies against the pinned key, so any swapped byte fails the chain and is refused.

The loader — the small, stable, pinnable entry

spaces/public/space-loader.js is the entry point. It is deliberately tiny and stable across app releases (it imports nothing versioned), so its hash is the one thing an extension has to pin. space.html loads it:

html
<script type="module" src="./space-loader.js" data-app="./space.js" data-bundle="./verified-app.json"></script>

Its exported verifyRelease() runs the chain above (pinned key → cert signature → frontendSha256 == sha256(manifest)) and returns run-or-exit. On a mismatch the app is never imported and the user sees a plain "couldn't verify this app" page instead of running unknown code.

Two tiers

Verification is opt-in per load, so the default path is byte-identical to a normal boot and a stray cert outage can never brick startup.

Tier Trigger What it guarantees
A — verify-in-place ?vc=1 (or the verifier) The served frontend matches the signed release (run-or-exit). The app still loads as normal ES modules under script-src 'self'.
B — loader-only CSP ?vcbundle=1 (or the verifier) script-src '<loaderhash>' '<bundlehash>' … with no 'self'. Evil same-origin JS cannot run at all.

Tier A answers "is this the right release?" Tier B additionally answers "can anything else run?" — no. The verifier extension is what forces either tier so the origin can't opt out.

How Tier B runs a whole app under "no 'self'"

Under a loader-only CSP the browser won't load same-origin <script>s or import() modules — so the loader stops relying on the browser to fetch code and does it itself. It fetches the app as text, checks its hash, and injects it inline — which runs only because that hash is the second pin in script-src. From there the app is a single verified closure. Two reusable primitives extend the same guarantee to everything the app loads lazily:

  • __vcImport(url) — verified module import. The app's import('./x.js') is rewritten to this; it resolves the target to its signed *.bundle.js, fetches it, checks sha256 == the manifest entry, and imports it from a blob. Lazy feature chunks (the enclave route, the PDF worker) load this way.
  • __vcFetch(url) — verified bytes fetch. For a path covered by the signed manifest it fetches, hashes, checks, and returns a verified Response (or throws); for anything not in the manifest — a dynamic API call, live data — it passes through untouched (the egress-lock connect-src still governs). The manifest is the discriminator: release asset → verify; runtime data → pass through.

Workers get the same treatment: the loader pre-verifies each worker's bundle, starts it from a verified blob URL (so worker-src blob: admits only checked bytes), and hands it the signed manifest so the worker can __vcFetch-check anything its runtime pulls in — e.g. the on-device speech model's WebAssembly (code) and weights (data). The build stage tools/build-verified-app.mjs bundles the app and each worker, rewrites new Worker/import() to the verified shims, and writes a signed verified-app.json sidecar.

Third-party assets are frozen, then verified

Large third-party binaries — the transformers.js runtime, the ONNX-runtime WebAssembly, the Whisper model — used to be fetched at deploy from moving CDN/registry refs with no integrity check, so a deploy would sign whatever the CDN served that day. They are now frozen: fetch-whisper.sh verifies every byte against a recorded pin (tools/whisper-assets.sha256) and aborts on drift, and the manifest covers them so __vcFetch can check them at load. The WebAssembly is code and must be verified; the model weights are data (a swap yields a wrong transcription, not code execution) — both are verified here regardless.

The pinned bundle — collapsing the client to one shell

Status: DESIGN, not built. Live today: the egress-lock CSP, the signed reproducible render build, /cert.json, and CSP-sha256 pinning of the shell's inline script. Not yet shipped: extending that pin to the shell's imported module closure via a bootstrap loader + signed client manifest. It is the web-side complement to the attested client and the piece that lets the reproducible-build proof (verify Tests 6–7) cover the code the browser actually executes, not just the render and inline shell.

The gap, precisely. The /space page is a small HTML shell: an inline app module (CSP hash-pinned) plus import statements pulling in ~23 sibling modules — e2ee.js (your encryption), recovery.js, backupVault.js, and the rest. Those imports pin the names of what loads, not the bytes; the modules load under script-src 'self' (same-origin, not content-pinned). So a swapped e2ee.js survives a shell-only check, the egress lock doesn't catch it (it can fold your key into an allow-listed request, or simply weaken the crypto), and an external curl | sha256sum verifies a different fetch than your session (split-view).

This is not self-attestation — it is a bootstrap. A page cannot verify itself. This design does something narrower and achievable: it collapses the client's verifiable surface from ~23 moving files to one small, stable shell by having the shell content-pin everything it loads. It is measured boot for the web. The mechanism: the build signs {path: sha256} for the transitive module closure into /cert.json (shellSha256, closureSha256, modules[]); the shell replaces import './x.js' with loadVerified('./x.js') — fetch once → hash → compare → execute that exact copy via a blob: module URL, else fail-closed (one fetch, verify, run — never re-fetch, or split-view reopens at the module level); rebuilding published source yields the same manifest, so verify Tests 6–7 grow to cover the whole client closure. A swapped module then fails to load rather than silently running. The shell's own bytes still need an external anchor — browser CSP + signed hash today; transparency gossip or a native/extension verifier to fully close it.

Where trust bottoms out, and honest residuals

Root For
The pinned platform key (published, out-of-band) the whole signature chain
The loader's hash (the one thing the verifier pins) that only the real loader is the entry
Math — signatures and SHA-256 every byte check
The browser + extension running faithfully the enforcement itself

Witbitz vouches for nothing about itself that isn't checkable against a key it doesn't control at verification time. The residuals, stated once for the whole page: verification is integrity, not correctness — a genuine bug in honestly-signed code is orthogonal, so receiver hardening (XSS-safe render, sandboxed widgets, validated tool inputs) still matters; without the verifier extension a naked browser has no out-of-band signal that the loader ran honestly (the toolbar badge is what a page can't forge); and attestation protects the code and the key, not the fact that plaintext reaches a model provider — the strongest-privacy tier still needs a BYO or enclave-hosted model (the attested tier).


Part 2 — Confinement: who can touch the key

The verification plane answers what code runs. This plane answers the other half — who can touch the room key — and it is the older, load-bearing idea, live in production: room-key custody lives in an outer frame the app cannot read.

The one idea, and the vault sandwich

Room-key custody moves out of the app frame into an outer frame the app cannot read. A vault (its own origin) holds the account master and the room keyring and frames the keyless app as an iframe. The app boots with no key in its URL and no key in its storage; the keys it needs arrive by a handshake it can't widen.

code
  vault  (L1 — holds the master + room keys, top frame, un-framable)
    │  frames the app with  ?fullapp=1&vcp=1&evictmk=1   (NO key in the URL)
    ▼
  the app  (L2 — separate origin, keyless: its live key variable stays null)
    └── ready{room}  →  key{room, mk}     (postMessage, origin-pinned both ways)
Layer Realm / origin Holds Never holds
L1 vault the vault origin (top frame) account master, room keyring, recovery UI, identity ceremony, the login/account pixels
L1 Kernel an L1 Web Worker (separate heap) room → mk and all mk-crypto full topology, network egress
L2 app the app origin (iframe, keyless) the view-models it renders; its live key variable is null the master, the keyring, the vault's storage

Three inversions versus the naive "put the key in the app's URL" design: the outer frame is the trusted one; the key arrives by handshake, not a URL fragment; the app is a separate origin, so the Same-Origin Policy is the wall, not the app's good behavior. The app is the same build as standalone — a runtime flag (IN_VAULT) selects the confined behavior, and the vault forces evictmk=1 so the current room's key is never a live main-thread variable. The vault is itself un-framable (frame-ancestors 'none' + X-Frame-Options: DENY); the login and account pixels have to live in it, because a cross-origin iframe can't run FedCM / Google sign-in / the recovery overlay — the ceremonies that touch the master.

Why the outer frame, not just discipline. A single-origin app can intend to keep its key from its render code, but nothing enforces it — an XSS or supply-chain slip in any module reads the same-origin localStorage and the live key. Splitting the vault onto its own origin makes the boundary browser-enforced: the app frame's window.top.localStorage throws SecurityError. That is the difference between "we're careful" and "the platform won't let it."

How the app gets keys without holding them. Two paths, both keeping custody outside the app's persistent reach — on demand (boot keyless, post ready{room}, the vault answers key{room, mk} fed to the Kernel worker) and in bulk (the account's index room carries room → mk for every Space; the Kernel absorbs every key from the index doc worker-side, which is why a new device needs only the index-room pointer to open everything). The full app↔vault channel is a small origin-pinned postMessage protocol (vcHandoff), every message gated on the exact peer origin.

The kernel and Atlas

Inside the vault sandwich, the key never sits as a plain variable — it lives in a Kernel: a Web Worker that holds every room key and does all the crypto, taking semantic requests in and never letting a key back out. Atlas is the sibling idea for the metadata (the room graph).

Status. The Kernel is built and, for key-learning from the index, default-on in production. The keyless-index mechanic ships; the full metadata-confinement Atlas realm is a deliberate non-goal for today's threat model and a documented target beyond it.

The organizing principle is least authority by information class — cryptographic secrets ≠ sensitive metadata ≠ presentation ≠ transport — so a compromise of one exposes only its own class:

Part Realm Holds Never holds
Kernel an L1 Worker (separate heap) room → mk, all mk-crypto full topology, network egress
Atlas a sibling L1 Worker the topology graph (rooms, titles, lane/pipe edges) raw keys, network egress
L1 broker the loader's main thread capabilities/ports, opaque ciphertext in transit keys, topology plaintext, view-models
App the L2 iframe the scoped view-models it renders keys, the full graph

The Kernel (l1-worker.js) takes semantic ops — poll, submitTurn, readState, seal, unseal — and does the mk-crypto and entry-signing inside its own realm; the key never posts back out, so a render-layer compromise can ask the Kernel to work but can't extract the material. Stated precisely so it isn't overclaimed: a Worker is a separate JS realm and heap with no direct object-reference access — not physical memory isolation; the boundary is only as strong as what's forbidden across it (no SharedArrayBuffer, no key-bearing transferables returned, no debug interfaces left on).

Atlas is used two ways. The keyless-index mechanic (built, default-on, ?atlasidx): the index room's room → mk pointers would leak keys if surfaced, so the Kernel absorbs them on unseal and returns a keyless graph, and injects them on seal so the app never touches a raw pointer even on write. The full metadata-confinement realm (target, descoped) would give the graph its own Worker and hand the app only keyless projections — closing the residual where a compromised render layer can read your Space graph (titles, lane membership) — but it's an expensive projection-completeness grind, and for the current threat model it's a non-goal: the app is the file browser and may see the directory listing; only the contents' keys are confined.

The strongest anti-exfiltration lever: L1 is the sole egress. Only the L1 broker has network access, and it moves opaque ciphertext it cannot interpret; the Kernel and Atlas can interpret sensitive data but have no ambient network access. So the components that can understand the data cannot send it; the component that can send cannot understand it — which directly closes the covert-channel exfiltration residual. Capability discipline hardens the ports as they mature: scope bound to the port not a parameter (poll() with no room argument, so a caller can't widen authority); explicit revocation (every capability is revocable, a membership/room change bumps the epoch); and bootstrap → LOCK (broad setup interfaces disappear after a short boot phase — privilege-dropping as in OS design). Background notifications must not reintroduce a raw key on the Service Worker: the design derives a one-way notification key NK = HKDF(mk, "notif") scoped to short previews, epoch-rotated and per-device revocable.


Part 3 — The attested client (native)

Status: the server-side seam is Implemented (built + tested, behavior-neutral, not enabled); the native clients are Design. A room's owner-signed policy can carry requireAttestation, and the render verifies a freshness-bound platform attestation against it (bound to the member's signing key so it can't be replayed) — implemented and tested with a mock verifier. What still needs the native side: the reproducible native shells and the real App Attest / Play Integrity / Keystore / TPM verifiers (today stubbed and fail-closed, so a requireAttestation room refuses every turn — the seam is ready for the verifiers to drop in). This is the missing symmetric half of the server attestation the rest of the platform already does.

You cannot verify the code a browser is running — the user owns the runtime; any check a page runs on itself can be hooked, and nothing stops a user from calling the API with their own client. The response is not to attest the browser (unwinnable) but to offer, opt-in, a client whose code can be attested: the exact static, egress-locked, reproducibly-built web bundle, wrapped in a thin native shell whose only jobs are to (1) present a hardware attestation, (2) hold keys in the Secure Enclave / StrongBox / TPM so the room key never sits in JS, (3) load the bundle locally into a locked web view, and (4) freshness-bind the attestation to each action. One source of truth across web and native; the audit surface is a thin generic shell plus the web code reviewers already read.

The one rule that makes it real, not theater: the web bundle must ship inside the attested binary and load locally — never off the network. Bundled and loaded from a local scheme, the app's signature covers its embedded assets, so an attestation of "genuine, unmodified app" includes the bundle. A web view pointed at a live URL (including Android Trusted Web Activities) attests the shell, which then runs whatever the network hands it — worse than a plain browser, because it launders trust it never earned. And the attestation must bind to every action, not once at admission, or a member attests with the genuine app then switches to a patched client for the writes that follow — the room's existing freshness challenge is the hook.

Platform Shell Attestation Keys Notes
Android WebView Play Integrity, or hardware Keystore attestation (X.509 rooted in Google, carries app-signing digest + Verified Boot) StrongBox / TEE Cleanest full chain. Self-distributed + reproducible APK + Keystore attestation gives source → binary → attested.
iOS / iPadOS / macOS WKWebView App Attest (hardware-backed) Secure Enclave Integrity strong; reproducibility partial — Apple re-signs the shipped binary, so you lean on a transparency-logged build pipeline for provenance.
Windows WebView2 Assembled — TPM 2.0 measured boot + Azure Attestation · TPM key attestation · Authenticode/MSIX + optional WDAC TPM 2.0 No single "App Attest for Win32"; strongest on a managed enterprise fleet (MDM + WDAC).

So integrity of the running bundle is attestable on every platform; end-to-end reproducibility of the shipped binary is Android-clean, iOS-partial, Windows-DIY — state that split plainly. The egress lock generalizes: today it is the CSP header; with native it splits into one canonical signed policy (the room-data path goes only to the declared origin; the shell reaches only a small fixed set of platform endpoints — attestation provider, push) and a per-platform enforcement of it. Every native OS control is coarser than connect-src, so on native the fine-grained guarantee leans on the reproducible, attested binary rather than a readable header: on the web you read a header; on native you reproduce and attest the binary. The web is not the weak link in the egress story — it's the platform with the cleanest, self-verifiable lock.

How a room requires it. "Only attested clients may join" is not a client-asserted flag — it's an admission condition set by the room/app owner and verified on the server, in the same slot that carries "only my registered users" (the owner rule). At join and on each write the server checks the attestation, bound to the member's signing key; other members can be shown a participant is on an attested client. Admission and client-integrity become one policy: { authenticated identity } AND { attested client }. It shrinks the hardest residual — content injection between members — without making the client omnipotent-proof (a rooted device, a bug in the genuine app, and untrusted content from others remain, so receiver hardening still matters). Because requiring an install trades away open-a-link reach, it is opt-in and additive: web rooms keep the untrusted-client architecture; attested-native rooms get this stronger tier on top.


Part 4 — The verifier extension

The verified client can check itself, but a naked browser has no out-of-band way to know the check ran honestly — an origin could serve a loader that lies. The verifier is a small browser extension that removes that last assumption: it lives in the toolbar, outside the page, forces verification the origin can't opt out of, pins the loader by hash, and shows a badge the page cannot forge. It ships in the repo at docs/verified-client-real/extension/.

Status. Built and proven headless against the deployed staging app (Chromium, DNR-forced CSP, CDP-confirmed). This is the shipped CSP-loader approach; the transparency-log and enclave extensions that go further are the power-user stack roadmap.

The shape is a per-brand, single-origin verifier whose host_permissions are scoped to one app origin — e.g. a bank's own "Bank X Verified Client." Minimal blast radius, a trivial least-privilege review, and the brand owns the whole chain. The same generic core is just configured with { origin, pinnedKey, bundleMode }. It does two independent jobs, both at the network layer: (1) force verification — a declarativeNetRequest redirect adds ?vc=1/?vcbundle=1 to every /space navigation, so the origin can never serve the app unverified; (2) force the policy — a modifyHeaders rule sets the CSP the renderer sees (the origin's strict policy in Tier A, upgrading a report-only egress-lock to enforced; the loader-only policy in Tier B, no 'self', worker-src blob:). CSP only ever intersects tighter, so a forced strict policy is a floor the origin cannot loosen. Alongside that it independently attests the release — running the same pinned-key → cert → manifest chain itself — and paints a toolbar badge (green ✓ / red !) the page cannot draw.

Why it works on mainstream Chromium (no response-body reading). An earlier design required Firefox and webRequest.filterResponseData to hash the exact bytes the browser loaded (Chromium MV3 can't read response bodies). The CSP-loader model sidesteps that: instead of observing the bytes, the extension forces a CSP that pins the loader's hash, and the loader verifies everything else — there is no byte to race, only the pinned loader can be the entry. That makes it a standard MV3 extension using declarativeNetRequest (applied before the renderer sees the response). The DNR rules are pre-armed at startup so the first navigation is already covered — no uncovered first load, no reload.

Honest residuals. A page's own service worker can synthesize a navigation response that never traverses the network, so the verified policy must include SW disposition (approve a specific SW hash or forbid one), enforced on-origin; the origin-served loader is the cost of keeping the brand's real domain (a chrome-extension:// page would break passkeys, cookies, TLS identity). The badge is the user's ground truth, but a naked browser has the same phishing surface as any site — the extension is the additive apex for people who want to check.

Install the verifier — a five-minute walkthrough

The extension in docs/verified-client-real/extension/ runs today on Chromium (128+) and enforces against the staging app (preview.witbitz-spaces.pages.dev). It's an unpacked developer build, not yet a Web Store listing.

  1. Point it at the app origin. config.json is the entire app-specific surface: origin (the app you want verified), pinnedKey (the platform signing key from verify it yourself — the one trust anchor, compared out-of-band), and bundleMode (true forces Tier B, loader-only CSP; false is Tier A verify-in-place).
  2. Load it unpacked. chrome://extensionsDeveloper mode on → Load unpacked → select the extension/ folder. The service worker starts and pre-arms its rules immediately.
  3. Open a Space and watch it verify. Navigate to …/space. Without doing anything else you see: the URL redirect to …?vcbundle=1 (Tier B) or …?vc=1 (Tier A); the app boot normally (the loader fetched the bundle and workers, checked each hash against the signed manifest, and injected them); the toolbar badge turn green ✓.
  4. Confirm it's really enforcing. Turn the extension off and reload — the URL stays /space and the badge is gone (the contrast is the proof). Inspect the /space response headers with the extension on: a content-security-policy with no 'self' in script-src (Tier B) and the report-only header removed. If the release ever fails the chain — a swapped byte, a bad signature — the loader shows "couldn't verify this app" and the badge goes red.
  5. Verify the extension itself (the one out-of-band act). The whole system hangs on trusting the verifier, so check it once: read its four small files, confirm the pinnedKey matches verify it yourself, and — for the strongest anchor — prefer an extension authored by someone other than the app vendor, so it isn't the vendor vouching for itself. That single check, done once, is the only thing you take on faith.

Part 5 — The power-user stack

Status: TARGET ARCHITECTURE. It composes shipped pieces (signed /cert.json, reproducible render build, readable egress-lock, the mkseal/mkUnseal wire format) with designed ones (the pinned bundle) and net-new ones (a filterResponseData verifier extension, transparency-log publishing + gossip, and the render in an attested enclave — the attested tier). It is the honest answer to "how does a user know they run a good app?" for a user willing to run one extra tool.

Who it's for: the power user who will install Firefox (Android or desktop) and one verifier extension — because a mobile web page alone cannot verify itself and an operator serving its own app proves nothing. The mainstream reach (open-a-link, any browser) keeps the existing model; this is the additive apex. The property that makes it "perfect": one deliberate trust decision, verified out-of-band once — then everything is checked automatically, on the real bytes, from browser chrome, with cheating detectable. Witbitz is removed from the trust base.

The five components: (1) a verifier extension — open-source, reproducible, ideally authored by someone other than Witbitz, trust anchors pinned inside it, using webRequest.filterResponseData in Firefox to hash the exact bytes the browser loaded (the same fetch the page uses — what makes it split-view-proof); (2) the pinned-bundle client; (3) the signed /cert.json (shellSha256, closureSha256, modules[], lambdaCodeSha256, sourceSha256, gitCommit, egress, enclave); (4) transparency logs — every build's hashes to ≥2 independent append-only logs; the extension checks inclusion and gossips signed tree heads, so a split-view is detectable by any one honest observer; (5) the enclave (Nitro) — the render runs attested, the extension verifies PCR0 == the reproduced measurement chaining to the Nitro root.

Per-load flow (automatic after a one-time setup): hash the shell + every module the browser actually loaded → verify /cert.json against the pinned pubkey → loaded hashes == the signed manifest → hashes are in the transparency logs with no split-view → verify the enclave attestation → confirm connect-src == the signed egress → badge green/red. Trust bottoms out entirely outside Witbitz — Mozilla, the independent extension author, the AWS Nitro root, independent transparency logs, and math. Not zero-trust — minimal trust, every deviation detectable and attributable. The likeliest overclaim, stated plainly: attestation protects the key and the code, not the fact that plaintext reaches a model provider — the strongest-privacy tier still needs a BYO/enclave-hosted model. And on iPhone, until the EU's alternative-engine rules give iOS a Firefox-grade extension API, iOS web can't do the byte-level check — an iOS power user's strong path today is the native App Attest app (Part 3).


Part 6 — The certified app

Status: DESIGN / direction. An answer to "how do I know an app is good?" for apps that are not open-sourced, by making the app model small enough that an automated, attested audit is sound. It defines what good means; the power-user stack verifies you received the good build.

Open source + plural human review is the gold standard for "no hidden backdoor," but it demands publishing the source, which most builders won't do. Can you get a verifiable "this app is good" without open-sourcing it? The move: don't out-analyse an adversary's arbitrary code — remove arbitrary code from the problem. A Witbitz app is almost entirely a manifest + UI — a declarative capability request (what data, which tools, where it may send) plus a constrained interface, closer to a policy than a program, and policy is checkable. Complexity disqualifies: an app too complex to audit cleanly is auto-rejected, which kills the main evasion vector for free (obfuscation, time bombs, hidden data-dependent paths are all complexity, and you can't hide a backdoor in convolution if convolution itself fails the gate). This converts "determine what an arbitrary program does" (undecidable) into "check a small declarative artifact against a policy" (tractable, much of it deterministic).

The mechanism: a fixed, confining runtime proven once for all apps (it confines every app to its declared capabilities, keeps the egress-lock, sandboxes the UI, forbids arbitrary code — one small shared artifact, openable and attestable once); plural attested checkers from different entities with different methods, each receiving the source inside an attested enclave (fed to the machines, not the public) and verifying the app matches its privacy certificate, with N-of-M agreement; deterministic where possible (declared egress and capabilities checked with no model), LLM where necessary (does the UI match the stated purpose); and a public verdict log — build hash + privacy certificate + checker set + verdicts — so "N independent attested checkers approved this build" is accountable and challengeable without publishing source. The trust moves from "audit each app's arbitrary code" to "a small fixed runtime + an objective complexity gate + plural attested checkers" — smaller, mostly deterministic, mostly openable. Hold the claim tight: the defensible guarantee is "a simple, capability-confined app, approved by N independent attested checkers against its declared certificate," not "any app is safe."


Next


The attested tier — "unobservable in use"

Status: Production — and independently verifiable. A Space can run its turns inside a Nitro enclave in production today: turn on "Run this Space inside the enclave," and its key is sealed to the measured image below. Every measurement is one you can run yourself. It is the app builder's per-room choice — the platform can run any room in the enclave — and since 2026-08 the tier also closes the last reader this page used to concede: the model call can only leave the measured image through an attested no-log gateway into confidential-compute inference (see the confidential model below — the trade is an open-weights model, not the frontier vendor). A running enclave publishes its measurement at /enclave-claim.json (the PCR pin the client verifies before it seals anything); see Verify It Yourself for the full ladder of live checks.

Everything else on this site proves things about code we publish: the app's egress is locked, the store holds only ciphertext, the deployed render is a reproducible build of source you can read. All of it stops at one wall.

None of it proves what the machine actually loaded. Between the ciphertext arriving and the answer going back, plaintext exists in memory on a server we operate. Read Verify It Yourself closely and it says so: the one thing you cannot check is that the render is unobservable in use.

This is the tier that closes that.

What changes

Your room key already travels sealed to a service public key rather than in the clear (spaces/public/mkseal.js). That module was written for this moment, and said so in its own header: today the private half lives in the service; tomorrow it can live only inside an attested enclave, and nothing on this side changes.

In the attested tier, that recipient keypair is generated inside an AWS Nitro enclave, at boot, and never written or exported. Its public half is bound into a hardware-signed attestation document. Verify the document, and you are sealing your room key to a key that provably exists only inside a specific, measured image.

"We cannot read your room" stops being a policy and becomes a property.

The four things you can check

Nothing here asks you to trust us, and none of it requires our cooperation beyond us publishing what we already do.

1. The document is genuine AWS hardware. A Nitro attestation document is signed by a key in hardware the operator cannot reach. Our verifier (spaces/public/nitroVerify.js) checks the COSE signature and walks the certificate chain to the AWS Nitro root G1, pinned in the source — the document carries its own copy of that root and we discard it, because a verifier that trusts the anchor its input supplied is not verifying anything.

It runs in your browser. WebCrypto cannot walk an X.509 chain, so the chain walker is written out in spaces/public/x509.js — about 180 lines, ECDSA only.

2. The image is the one you can rebuild. PCR0 is a SHA-384 measurement of the running image. infra/enclave/build-eif.sh reproduces it from published source. Four things are pinned because each is an input: the base image by digest, SOURCE_DATE_EPOCH, buildkit by digest, and the nitro-cli version.

This is the part most projects skip. A measurement you cannot reproduce only tells you the image hasn't changed since we built it — which is the "trust us" the whole ladder exists to escape.

3. PCR8, from the certificate rather than our word for it. A signed image also reports PCR8, a measurement of the signing certificate. We publish the certificate, not just the number, so you derive PCR8 yourself: SHA-384(48 zero bytes ‖ SHA-384(certificate DER)). Our verifier prefers the value it computed and rejects a cert.json whose stated PCR8 contradicts its own certificate. A dishonest number is caught by arithmetic.

(AWS's own documentation describes PCR8 differently — as a hash of the certificate's "fingerprint" — which does not match their code and yields a different value. We follow the code, and confirmed it against real hardware.)

4. Liveness, if you ask for it. An attestation document with no challenge proves an enclave existed, not that it is the one answering you. Send a nonce; it comes back inside the signed document or the check fails. Our verifier reports fresh: false when no nonce was supplied, so a stale proof can never be presented as a live one.

What it still does not buy

This section is the point of the page. A security claim that only lists what it covers is marketing.

  • Model egress is attestation-gated, not invisible. The enclave establishes TLS itself, so neither we nor the parent instance can read a model call — and the measured image refuses to send the prompt anywhere except an attested confidential-inference route (below). But attestation cannot make computation unreadable to the thing computing it: some measured environment always decrypts the prompt to answer it. What this tier changes is that every such environment is now attested and each link verifies the next — not that no environment reads it.
  • PCR0 proves the image, not the behaviour. It proves the running code is the code you can rebuild. Whether that code does what we say is what reading it is for; reproducibility makes reading it worthwhile, not unnecessary.
  • Metadata remains. Which room is active and when, message sizes and timing — an enclave changes none of it.
  • The image is large. Running the existing pipeline means an interpreter, a libc and ~86 MB of dependencies, all measured. The measurement is exactly as sound; the audit is a much bigger job than reading two files. A minimal variant exists for cases where the smaller claim matters more than the existing code.
  • Availability is still ours to break. The parent instance can refuse to forward your request. It cannot read it.

Measured, not asserted

On real hardware, 2026-07-29 — and the last row is the one that matters, because it is the product rather than a component of it:

check result
attestation verified against the pinned AWS root, with our own nonce ok — PCRs 0/1/2/8, fresh: true
PCR0 vs an independent rebuild from published source match
PCR8 vs the published signing certificate match
room key sealed by the unmodified client, opened inside the enclave opened
the enclave asked to hand the key back it returns HMAC(key, challenge), never the key
a sealed blob replayed as a different operation refused
one ciphertext bit flipped refused
verification in a browser 1.8–22 ms
the full agent — model, tools, Places, map widget — answering in a real room works

That last line is not a reduced demo. It is the deployed handler, unmodified, with its tools, reached from a phone: the attestation verified in the browser, the room key sealed to a key born inside measured hardware, the reply generated in the enclave, sealed, stored on a parent that cannot open it, and rendered.

The verifier is also exercised against genuine attestation documents from unrelated production enclaves, so it is known to handle real AWS output and not merely our own.

Scale-to-zero — verifiable privacy that costs nothing idle

A confidential enclave used to mean a machine running around the clock — roughly $150 a month, whether or not anyone was in the room. That standing cost is exactly what made hardware-verified privacy expensive to leave running. It is gone.

  • Idle → $0. No enclave runs. The Space still opens: its history is served content-blind from sealed storage — the parent hands back ciphertext, your device opens it with mk — so an asleep Space is never a blank one.
  • Wake on interaction. Your first message (or approving a draft, or creating an enclave Space) signals a scheduled orchestrator, which launches an instance from a signed template. It self-provisions, pulls its measured image, opens its keys through the PCR8-gated KMS grant, and publishes its endpoint — no SSH, no operator step — in about three minutes. The client shows a "waking…" state and sends your message the moment the enclave attests.
  • Re-attest on every launch. Each launch generates a fresh in-enclave recipient key, so the client re-verifies the hardware attestation and re-pins before it seals — a relaunched enclave is proven anew, never assumed.
  • Reap when idle. About twenty minutes after the last real interaction the instance terminates itself and clears its endpoint. Passive reading never keeps it alive; only real turns do.

The point is not the saving. It is that leaving the tier available costs nothing when the room is quiet, so an app builder can offer verifiable privacy on any room without a standing bill.

Now switched on — and what is still left

This tier is Production: a Space can run inside an enclave in production today; the app builder enables the enclave per room. The parts that used to be "operational rather than unknown" are done — and worth stating precisely:

  • Routing and lifecycle — done, as scale-to-zero. What was one instance started by hand is the on-demand orchestrator above; the published measurement lives at /enclave-claim.json, which the client verifies and pins before sealing.
  • Secrets are released only to a measured enclave. Credentials are sealed under a KMS key that refuses to decrypt unless the request carries an attestation matching our signing identity (PCR8). An account administrator is denied. The host holding the IAM role, using the very credentials the enclave uses, is denied. The enclave is allowed. The parent hands over ciphertext and gets back a list of names — never a value.
  • Storage is durable and still unreadable. Sealed blobs live in S3. Read back with account-admin credentials, a conversation is an IV, a ciphertext, and one recipient: the room. Not the operator.

What genuinely remains is scope, not mechanism:

  • Per-room, by choice — not a platform gap. The platform can run any room in the enclave today; which rooms do is the app builder's per-room choice.
  • Audit surface. Running the existing pipeline means an image of ~250 MB rather than the 5.6 MB minimal variant. The measurement is exactly as sound; reading it is a much bigger job.
  • The model residual is closed on this tier — with two honest footnotes. The confidential-model axis below is shipped: the prompt reaches only attested environments end-to-end. The footnotes: verification of the model side is transitive (your one PCR check covers the measured code that verifies the gateway, which verifies the GPU workload — not a chip quote delivered to your browser), and nobody in the industry yet attests the model weights themselves ("these exact tensors") — the workload and serving stack are measured, the tensors are vouched.

A Space that runs in an enclave publishes its measurement at /enclave-claim.json, and the deployed web app verifies and pins it before sealing your key. This page now describes a property you can turn on, not only a capability.

The confidential model — closing the last reader (Shipped)

The bullet above used to name the one plaintext reader this tier did not remove: the model vendor. Since 2026-08 it does remove it. The render enclave no longer hands the prompt to a frontier vendor's API — the measured image declares a single model route: an attested, no-log inference gateway into confidential-compute inference on an open-weights model, with a hard floor (min_privacy: confidential) that refuses the request rather than routing it anywhere lesser.

A chain of attested links, each verifying the next. We buy the confidential-inference commodity and keep the verification (the make-or-buy call this axis was always going to face — the runtime is a commodity; the verifying is the product). Concretely: (1) the client verifies the render enclave's Nitro measurement, exactly as above; (2) the render enclave's measured code fetches and cryptographically verifies the gateway's live attestation — a Google Confidential Space document, Intel-TDX-rooted, secure-boot on, debug off, image digest bound to the gateway's open-source release — and refuses to send the prompt if any check fails; (3) the gateway's own measured, open-source code verifies the GPU workload's Intel TDX + NVIDIA confidential-computing evidence against pinned measurements and encrypts the prompt to the measured workload (ML-KEM + AEAD), failing closed. The prompt therefore exists only inside attested environments, end to end. The measured egress firewall makes the route exclusive: the enclave's allow-list — itself inside the measurement — carries the gateway, and the tier's key delivery carries only the gateway credential, so a frontier API is unreachable by construction, not by policy.

Two honest footnotes. First, verification of the model side is transitive: your one PCR check covers the code that verifies the next link, which verifies the next — a real chain of custody, but not a chip quote delivered to your browser (a direct-to-client model-enclave verification, as originally designed here, remains the strongest variant and a compatible upgrade). Second, nobody in the industry yet attests the weights — the machine, the serving stack, and the workload are measured; that the tensors inside are exactly the named model's is vouched by the attested stack, not measured. Both footnotes apply to every confidential-inference offering today, not just ours; we state them because a trust page that doesn't is marketing.

The honest trade — this one costs model quality. A model can only run confidentially if it can run in a TEE you can measure: an open-weights model (DeepSeek, Kimi, Qwen, GLM, gpt-oss and the like), not a frontier vendor's proprietary weights. Turning this axis on trades the frontier model for a confidential one — the tier's create toggle says so in as many words. The builder chooses per room: frontier, but the vendor sees the turn — or open, and every environment that touches the turn is measured. Frontier-confidential waits on a frontier vendor offering attested inference on its own side.

Status: Shipped. The confidential tier runs this route in production today — scale-to-zero render enclave, the attested gateway verified inside the measured image, confidential-compute inference behind the floor. It is the seventh privacy axis in Privacy tiers, and with it the double blind's last standing residual is reduced to the two footnotes above.

Read next: Verify It Yourself for the full ladder of live checks, and The double blind for the model this completes.


Deployment options

Witbitz can run as a hosted platform, inside a customer's cloud boundary, or in an air-gapped environment. The product claim shifts slightly in each mode — so this page keeps the deployment model explicit, then documents all three in one place: hosted (the private-beta path), your own boundary (enterprise / VPC), and air-gap (zero egress). The runtime is the same reproducible image in every mode; what changes is where its seams point and what a skeptic gets to check.


The options

HostedWitbitz operates the runtime; builders get tenant keys during private beta.
EnterpriseThe same runtime runs inside a customer cloud boundary.
Air-gapNo external egress; model and replacement services stay local.
Option Status Boundary Best fit
Hosted private beta Live, invite-only Witbitz-operated cloud Early builders and platform integrations.
Enterprise VPC / on-prem Runnable today Customer-controlled cloud or network Enterprise buyers who want their own model, storage, IdP, and ops.
Air-gap Runtime switch exists; self-hosted model quality is the remaining work No external egress Regulated, defense, government, and high-control deployments.

Choosing a mode

Need Start with
Build against the API quickly Hosted private beta
Keep data and model calls inside your tenant Enterprise VPC / on-prem
Prove no external egress Air-gap
Verify hosted privacy claims Hosted + the Verify page
Remove Witbitz as an operator On-prem

Hosted

Hosted is the default private-beta path. Witbitz operates the runtime and exposes the tenant-keyed /v1 API to invited integrators. The docs and API contracts are public; tenant keys are gated. This is the operating model for builders building on Witbitz-hosted infrastructure.

What you get

Surface Status
Spaces app Live
/space async runtime Live
Tenant-keyed /v1 API Private beta
OpenAPI production spec Public
Full design OpenAPI Public, status-labelled
Owner rule Implemented (not enabled by default)

Access

Register a coupon-free demo tenant straight from the Get Started walkthrough, or redeem an access code and create a tenant key pair at:

code
https://witbitz.chat/access

Keys are shown once. Secret keys (wsk_…) are server-side only. Publishable keys (wpk_…) are browser-safe identifiers for public reads and funnel-entry writes, but they do not authenticate an end user. If you do not have an access code, email hello@witbitz.chat.

API contract

The production /v1 API is documented openly:

code
https://api.witbitz.chat/v1/openapi.json
https://api.witbitz.chat/v1/openapi.full.json
https://api.witbitz.chat/v1/

During private beta the contract may change in coordination with invited integrators. Treat openapi.json as the production callable surface and openapi.full.json as the broader design map. See Platform API.

Hosted trust evidence

Hosted mode is designed to be checked from outside — every claim has a place you can inspect it:

Evidence Where
Admission gate refuses unauthorized reads/writes Verify it yourself
Stored ledger is sealed to a key we never hold op:'sealed'
App egress is browser-enforced Content-Security-Policy
Deployed render build is signed /cert.json
Published source reproduces the deployed hash /source.tar.gz and Test 7

The attested server tier is in production: a hosted Space can run its turns inside a Nitro enclave today (a toggle), with your key sealed to the attested image and the enclave scaling to zero when idle. Enabling it is the app builder's per-room choice — the platform can run any hosted room in the enclave.

What hosted can and can't see

Hosted Witbitz can still see metadata: room existence, request timing, ciphertext sizes, billing/metering events, and transient identity during gated verification. It cannot read the sealed ledger, because there is no operator recovery recipient — see Trust model.

When that metadata surface isn't enough — you need customer-controlled model calls, customer-owned storage and records, your IdP as the only identity authority, and no Witbitz-operated runtime in the path — move to your own boundary.


Your own boundary — enterprise & VPC

Status: shipped and runnable today (VPC tier). The entire Witbitz runtime — the app, the agent, the sealed ledger, admission — runs inside your own network, off our cloud, on infrastructure you control. One switch (AIRGAP=1) takes it to zero egress. The one piece still in progress is a self-hosted model for the fully air-gapped case (below).

Enterprises increasingly won't send their data — or their AI — to a third party. Witbitz answers that literally: you run the whole thing yourself. And the design keeps this from forking the product — every external dependency is a single environment switch that defaults to the hosted behavior when unset. The build we run for the public and the build you run in your tenant are the same reproducible image; you just point its seams at your own infrastructure. Same code, same verifiable-privacy properties, your boundary.

Two tiers

Tier Boundary Model Who it's for
VPC your cloud tenant; may reach your chosen model API + map services your own cloud key (OpenAI / Azure) most enterprise buyers
Air-gapped no egress at all a self-hosted model, in-boundary defense · government · regulated

The VPC tier is a complete, operable product now. The air-gapped tier is one switch away — except for the self-hosted model, which is the remaining substantial piece (see Air-gap below).

One command

The runtime ships as a single container plus a Compose stack that wires the whole thing — the app, an S3-compatible object store for the sealed ledger, the record store, and a reverse proxy with automatic TLS:

bash
cp space.env.example space.env          # your model key + a domain
docker compose up -d --build            # the whole stack, off our cloud
curl https://space.your-co.com/space-config

There is no per-app server to stand up and no dependency on our infrastructure. The app the browser loads is the same static, egress-locked, reproducibly-built page — now served from your host.

What each concern maps to

Every seam is a switch. Unset, it's the hosted default; set, it points at infrastructure you own.

Concern Runs on Switch
Model any OpenAI-compatible endpoint — your cloud key, Azure, or a self-hosted vLLM / Ollama LLM_BASE_URL
Object store (the sealed ledger) MinIO or any S3-compatible store S3_ENDPOINT
Records (Spaces + Bridges) a local store (single node) or Postgres (multi-node HA) DATABASE_URL
Secrets mounted files — a Kubernetes Secret or a Vault agent — instead of a cloud secrets manager SECRETS_BACKEND=file
Who may join your own identity provider — Okta, Entra, or Keycloak — as a trusted OIDC issuer SPACE_OIDC_ISSUERS
Operations backup / restore, a retention sweep, bounded log rotation included in the kit
Zero egress refuse every outbound path — see below AIRGAP=1

Your identity provider

An email-gated Space admits members who sign in with an issuer you trust. Register your corporate IdP and it's authoritative — no dependency on any consumer sign-in. It's additive and fail-closed: a registered issuer can't weaken the built-in ones, the audience is checked (so a token minted for another app can't be replayed), and the Space's own membership list still decides who's actually in. The IdP only proves identity; the Space decides admission. See Identity and admission.

Operate it

The record store has no silent auto-expiry, so retention is explicit: a sweep reaps Spaces idle past a window you set (records carry no timestamps — they hold no plaintext — so "idle" is measured from the last write). Backup captures the sealed ledger, the records, and the secrets in one archive. Logs rotate with a bound and stream to your stack (Loki / ELK / your SIEM). For high availability, switch the record store to Postgres and run more than one node.

The trust story gets stronger, not weaker

Hosted, our signed /cert.json and reproducible build let a skeptic confirm the operator — us — is running exactly the published, readable source, byte-for-byte (Tests 6–7), with the app's egress locked (Test 4). That we are blind to your data in use — unable to read the plaintext during the transient decrypt-and-render instant — is the attested server tier, now in production: turn on the enclave and verify it against the running measurement. On-prem there is no third-party operator at all, so the cert's meaning inverts into supply-chain proof: the image you are running is byte-for-byte the audited source — nothing was smuggled in. This is where verifiable privacy is at its strongest: both halves are checkable by you, on hardware you control, without trusting us at all.

Verify a running deployment

bash
# the egress allowlist, as a header — in air-gap it names no external host:
curl -sD- https://space.your-co.com/ -o /dev/null | grep -i content-security-policy
# the exact build the box is running:
curl -sXPOST https://space.your-co.com/space -H 'content-type: application/json' -d '{"op":"attest"}'

On-prem and air-gapped deployments are handled as enterprise engagements — hello@witbitz.chat.


Air-gap — zero egress

Air-gap mode is the deployment setting where Witbitz refuses every external network path it can control. It is for environments where "no data leaves" must be a property you can inspect, not a vendor promise. The short version: set AIRGAP=1, point the model at an in-boundary endpoint, and verify the browser and server both name no outside hosts.

Status. The runtime has a zero-egress switch today. The remaining substantial work for a fully useful air-gapped tier is not the Witbitz runtime itself — it is operating a self-hosted model with the quality the deployment requires, plus local replacements for any external data tools you want to keep.

What AIRGAP=1 changes

Layer Behavior
Browser Content-Security-Policy removes external hosts — the browser itself refuses any external fetch.
Server External map, route, photo, URL, search, and flight paths are refused at a single chokepoint.
Tools Internet-touching tools fail closed even if a Space configured them.
Model Must be pointed at an in-boundary endpoint; otherwise the turn still leaves the network (the runtime warns loudly at boot).

What still works

Chat, agent turns against an in-boundary model, document reading, PDF generation, charts and diagrams, shared widget state, IdP admission against an in-boundary identity provider, and backup / restore / retention / logs.

What is disabled or needs a local replacement

Public web search · external URL import · Google Places photos/search · map tiles, geocode, and routes (unless self-hosted) · flights lookup (unless backed by an in-boundary service) · remote model APIs.

Verify the boundary yourself

bash
# Browser boundary — in air-gap the CSP names no external host:
curl -sD- https://space.your-co.com/ -o /dev/null | grep -i content-security-policy

# Running build — compare to the published source and your own deploy process:
curl -sXPOST https://space.your-co.com/space -H 'content-type: application/json' -d '{"op":"attest"}'

The trust story

Hosted Witbitz asks you to verify the operator's deployment. Air-gapped Witbitz removes the third-party operator from the path, so the check becomes supply-chain oriented:

  • Is the image you run the audited source? (op:attest returns the exact build commit the box is running.)
  • Does the browser policy name no external host?
  • Do server-side tools refuse outbound access?
  • Is the model endpoint in-boundary?
  • Are logs, backups, and retention under your control?

Read next: Verify it yourself · Status · Trust model


Status — Production, Beta, Implemented, and Design

This page is the product map, and the source of truth for each mechanism's maturity. Witbitz uses four literal states — nothing else:

  • Production — running in production today. (Whether every Space has it structurally, or an app enables it per room, both are in production.)
  • Beta — usable by invited integrators; the contract may change.
  • Implemented — built and tested, the code exists, but not enabled in production.
  • Design — the architecture is specified, but the mechanism is not built.

Maturity is separate from two other facts, and this page never collapses them into the maturity word:

  • Scopewhose property it is: the platform, the first-party app, the client, or the server.
  • Configuration — whether every Space gets a mechanism (structural) or an app switches it on per room. That is a config fact, documented in the Spaces app defaultsnever a maturity qualifier. (The attested tier is Production, full stop; the per-room enable is a builder choice, not an "opt-in" that makes it less-than-production.)

Where a mechanism is Production in one place and Design in another, it is split into separate facets so no single label ever describes two states. For the privacy configuration spine — the canonical maximum, the mechanism axes, the named modes, and the feature × mode matrix — see Privacy tiers.


Summary

Capability Maturity Scope What to say
Async Spaces Production platform Persistent shared rooms with AI turns, sealed ledger, documents, widgets, and artifacts.
Private lanes Production platform Each participant can have their own lane — a separate Space, linked on the device; its agent observes the shared room and cannot post to it.
Progressive replies Production platform The answer is sealed and published as it is written, so a reader starts reading before the turn finishes.
Away notification Production platform Content-free Web Push wakes an installed PWA; the notification carries no message text.
Offline export Production client A Space exports to a file you keep, with a standalone decryptor and sealed per-Space metadata.
Admission gate Production platform Gated Spaces refuse reads and writes without an allow-listed identity.
Content-blind storage Production platform The store holds ciphertext sealed to the room key; no operator recovery recipient.
Delegated authority Production platform High-stakes actions require human approval before execution.
Egress lock Production platform The browser-visible app can reach only a published allowlist.
Signed build certificate Production platform /cert.json binds the deployed render build and egress footprint.
Reproducible render build Production platform Published source rebuilds to the deployed Lambda hash.
Attested server tier Production platform A Space runs its turns inside a measured Nitro enclave in production; the key is sealed to the attested image; launch-on-demand + scale-to-zero. The app builder enables the enclave per room.
Recovery — vault Production client One vault opened by either a passkey or a backup code, stored as opaque blobs at derived ids. No operator recovery key; losing every enrolled method is unrecoverable by design.
On-prem VPC tier Production platform The runtime runs in a customer's boundary with customer model, storage, and IdP seams.
Platform API Beta platform Tenant-keyed /v1 API; invite-only, public OpenAPI, contract may change.
Application SDK Beta client A small ES-module surface so a static page becomes a private, agent-backed, link-shared app; wraps the reference client.
Owner rule Implemented platform Owner-governed rooms and tiered admission are built and tested, but not enabled in production (inert unless configured).
Membership re-key — Bridge Implemented platform Adding or removing a member forks the room to a new epoch key; live in the Bridge, which is itself an early preview.
Verified client (web) Implemented client The verifier extension + CSP-loader that pins the release; built and proven on staging, gated behind ?vc — not the default production boot.
Bridge protocol Implemented platform Cross-organization governed collaboration exists as an early protocol surface.
Membership re-key — single-Space Implemented platform A membership change forks the Space to a new epoch key sealed only to the remaining keepers (removed member structurally excluded); built + wired to the Members UI + tested (agent/spaceFork.test.mjs). Fork-based; triggered by a membership change (no scheduled rotation). The server read-gate is a complementary, reversible revocation.
Social recovery (k-of-n) Design client k-of-n social recovery is designed, not built.
Attested client (native shells) Design client Native hardware-attested shells are not shipped; the server-side requireAttestation seam is Implemented (mock verifier), the native verifiers stubbed.
Companion Design client A native helper may improve notifications and key storage; the platform does not depend on it.
Confidential model (attested inference) Production platform The confidential tier routes its model call through an attested no-log gateway into confidential-compute inference on an open-weights model (The attested tier).
OHTTP (network-metadata) Production platform Oblivious transport (RFC 9458), behind the "Hide my IP" toggle: a pinned L1.5 client + a client-verified relay (GCP Confidential Space, relay.witbitz.chat) — no party sees both your IP and your rooms. Covers all app→AWS traffic incl. the enclave attest/wake path (fail-closed); relay verification is offline (Google's Confidential-Space keys baked into L1.5, so checking the relay never reveals your IP to Google) (Privacy tiers).
Client shaping Design platform Traffic-shaping (sizes/timing) is specified, not built.

Production

Async Spaces

The production Spaces app runs the async model: a member posts a turn, the render opens the sealed ledger, the agent answers, state is re-sealed, and the Space goes cold again. Idle Spaces do not require a running server process. Read: Async Spaces

Identity and admission

Spaces support key-possession membership and deployed signed identity for gated rooms. For an email-gated Space, reads and writes both require an allow-listed identity; a link holder without that identity receives 403. Read: Identity and admission and Verify it yourself

Delegated authority

Effectful agent actions go through propose, approve or deny, then execute. Execution is refused unless the proposal is approved and has not already run. The audit trail is sealed with the room. Read: Delegated authority

Verifiable privacy checks

The current production checks cover the admission gate, the app's egress footprint, content-blind storage, the signed build certificate, and a reproducible build of the published render source. Read: Verify it yourself

Attested server tier

A Space runs its turns inside an AWS Nitro enclave in production. The room key is sealed to a key generated inside the measured image and bound into a hardware-signed attestation that the deployed web app verifies and pins before it seals anything. Runtime secrets reach the enclave only through a PCR8-gated KMS grant — the measured image is the only thing that can decrypt them. It is made affordable by scale-to-zero: the enclave launches on demand, wakes on your first message, shows the Space's history while it sleeps, and terminates itself about twenty minutes after the last interaction. The app builder enables the enclave per room — the platform can run any room in it — and the model call leaves the measured image only through an attested no-log gateway into confidential-compute inference on an open-weights model — the frontier-vendor residual applies only to rooms that choose the frontier trade (the attested tier). Read: The attested tier

Beta

Platform API

The /v1 API is tenant-keyed and documented with a public OpenAPI contract. Access is invite-only during private beta; the contract may change with invited integrators before general availability. Read: Platform API

Application SDK

A small ES-module surface (Witbitz/Space) so a static page becomes a private, agent-backed, link-shared app; it wraps the exact reference client production runs. Usable today; the surface may still change. Read: The SDK

Implemented (built, not enabled in production)

Owner rule

The owner rule lets an app owner mandate signed admission policy and tiered capabilities for every governed room. The implementation is built and tested but inert: unset keys leave existing rooms unchanged, so it is not enabled in production. Read: The owner rule

Verified client (web)

The web verifier extension + CSP-loader pins the platform-signed release and forces the egress-lock in the running browser. It is built and proven headless on staging, gated behind ?vc / the extension — not the default production boot. Read: The verified client

Membership re-key (Bridge) · Bridge protocol

In the Bridge, adding or removing a member forks the room to a new epoch key the removed member cannot derive. The Bridge itself is an early cross-organization protocol surface. Read: Identity and admission

Design

Attested client (native shells)

The server-side admission seam exists and is tested with a mock verifier, but the native attested-client shells are not shipped and the real hardware verifiers are stubbed. The rule for the future client is that the reproducible web bundle must be bundled inside the attested binary and loaded locally. Read: The attested client

Social recovery · client shaping

k-of-n social recovery and traffic-shaping are each specified but not built. (Single-Space membership re-key shipped — see the matrix above; the confidential model and OHTTP shipped too — Privacy tiers.)

Machine-readable source: llms-full.txt · Generated 2026-08-29T18:46:30Z · build f3ff88cc · every doc in one fetch: llms-full.txt (HTML)