@xdevplatform/chat-xdk). Private identity and signing keys stay on the user’s device. Your servers (and X) only ever see ciphertext, public keys, and OAuth tokens—not the PIN or private key material used to encrypt and sign messages.
This page is the recommended architecture for client apps. For PIN and key-handling rules that apply to every app type, see Handling private keys.
Why WASM for UI apps
End users should never paste their encryption PIN into your backend, and your backend should never hold their identity private keys. If a third-party server receives a user’s PIN or root private keys, that party can decrypt conversation keys wrapped to that identity even after the user revokes OAuth access. Client-side WASM avoids that class of failure for legitimate apps.
Recommended architecture
Split crypto (browser) from API transport (your backend or direct X API with a user token):
A common pattern (used by internal demos such as browser chat clients) is: WASM + React (or similar) in the frontend, TypeScript XDK in Next.js (or other) API routes so the browser never talks to
api.x.com with a long-lived secret if you prefer not to. Crypto still runs only in the browser.
Install the browser package
@xdevplatform/chat-xdk; there is no separate Rust toolchain for consumers. Requires a modern browser (and Node.js 18+ if you share code with SSR—run crypto only on the client).
Session flow (PIN once, keys stay in memory)
Do not prompt for the PIN on every message. Unlock once per browser session, keep theChat instance in memory (module singleton, React context, etc.), then encrypt and decrypt against that unlocked instance.
UX expectations
What your server is allowed to see
Realm tokens for Juicebox are not the user’s PIN. They authorize the backup protocol for that user and key version. Keep minting them on a backend that already holds the user OAuth context.
Secure key backup in the browser
Client apps should use secure key backup (setup / unlock with a passcode), not a raw key file:
- Load
juicebox_configfrom the user’s public-key record (public_key.fields=juicebox_config). createChat({ juiceboxConfig, getAuthToken }).- First time:
generateKeypairs→ register public keys with X →setup(pin). - Later:
unlock(pin)on this device (or a new device with the same PIN).
createChat surface, so application JavaScript is not encouraged to pull root key bytes into the page. Prefer that model over hand-rolled localStorage key dumps.
Full registration and unlock steps: Getting Started. Concepts: Cryptography primer.
Browser hardening checklist
- Run Chat XDK only in client bundles (no SSR of unlocked keys).
- Treat the unlocked
Chatinstance like a live session secret: do not put it onwindow, do not log it, do not post it to analytics. - Defend against XSS: CSP, careful
dangerouslySetInnerHTML/ markdown rendering, dependency hygiene. XSS in a chat app can reach keys in memory even when keys never hit the network. - Use HTTPS everywhere; never mix crypto pages with insecure scripts.
- Prefer minimal OAuth scopes; request DM scopes only when needed and explain them in your product UI.
- On logout, call
lock()/free()and discard the instance.
localStorage, how to think about session persistence) are in Handling private keys.
Next steps
- Handling private keys — PIN warnings, storage, bots vs UI apps
- Getting Started — full key registration and first message
- Chat XDK — API reference for
createChat, encrypt, decrypt - Real-time events — deliver ciphertext to the client for local decrypt