Client boundaries
Better Auth helpers vs vault and chat — both products use zeroAccessClient, and elevate only when a write needs sudo.
Client boundaries
Better Auth plugin clients are small HTTP helpers. Vault and chat are additional product packages. They do not ship their own auth HTTP. They call authClient.zeroAccess, and authClient.elevate only when a write needs sudo.
Two rings
authClient is not a key manager and not a messenger. Elevate never opens a
vault. Vault unlock never mints sudo. Chat never encrypts because elevate
succeeded.
Better Auth helpers (one createAuthClient)
passkeyClient login / register
loginFactorsClient types only
elevateClient sudo ceremony
zeroAccessClient wraps, recovery, e2e relay HTTP
Product (additional)
zero-vault local MEK → publishes via zeroAccess
zero-e2e identity from vault → relay via zeroAccessCanonical engineer spec: monorepo docs/CLIENT_BOUNDARIES.md.
Better Auth helpers
| Plugin | Owns |
|---|---|
Stock passkeyClient() | Login / credential HTTP |
loginFactorsClient() | session.loginFactor types — no HTTP |
elevateClient() | /elevate/* — L1 sudo |
zeroAccessClient() | /zero-access/* — wraps, recovery, chat relay |
zeroAccessPasskeyClient() is a no-op. Use stock passkeyClient() plus PRF helpers.
Elevate (elevateClient)
Step-up on an existing session. Default 300s. Verify JSON is UI-only; server requireElevate / requireAccess authorize.
| Call | Does |
|---|---|
elevate.methods() | Enrolled factors — UI |
elevate.verify({ method, … }) | Password, TOTP, or passkey assertion |
elevate.withPasskey() | Options → credentials.get → verify |
Not this client: login, enroll, MEK, PRF, chat.
When vault / chat call it
| Action | Elevate? |
|---|---|
| Daily vault unlock | No |
| Persist wraps / recovery enroll | Usually (host assertAccess) |
| Chat send / inbox | No (session on relay) |
| Chat identity rotate | If the host gates that write |
Products call authClient.elevate.* then authClient.zeroAccess.*. Unlock and sealMessage must not mint sudo.
Three passkey ceremonies
| Call | Layer | PRF? |
|---|---|---|
Stock passkeyClient() | L0 identity | no |
authClient.elevate.withPasskey() | L1 sudo | no |
vault.unlockWithPasskey() | L2 unwrap MEK | yes |
Zero-access (zeroAccessClient)
Shared persistence for both products: MEK wraps, account recovery, and e2e prekey/inbox/contacts. The server never holds MEK, mnemonic, PRF, vault password, or message plaintext.
Relay HTTP belongs here — not a chatClient() plugin. Use authClient.zeroAccess.putE2ePrekey (and siblings) from the typed client.
Vault (createVaultClient)
Not a BA plugin. Local create / unlock / reset. Wire: authClient.zeroAccess to publish wraps; authClient.elevate before privileged writes.
createVault has no HTTP (show the recovery phrase first). Publishing is storeMek + putWrapSlot (the persistCreatedVault helper is sugar over those). Daily unlock is L2 only.
Chat (@railman/zero-e2e)
Not a BA plugin. Crypto only (X3DH + Double Ratchet). Identity seed from a vault grant. Delivery through the same zeroAccessClient e2e routes. No second WebAuthn; no elevate-to-encrypt.