# Authentication Cherry chat identifies users by their Solana wallet. The SDK supports three auth modes: you pick one when you [configure the embed](https://portal.cherry.fun/docs/embed/configuration.md) in the portal, and wire the matching client code. Choose based on whether you run a backend and whether you want users to prove wallet ownership. | Mode | Backend? | Wallet signature? | Use when | |---|---|---|---| | `wallet-only` | No | Yes (user signs) | No backend; users bring their own wallet. Public rooms. | | `app-trusted+wallet` | Yes | Yes (user signs) | You run a backend and want verified wallet identity. **Most common.** | | `app-trusted` | Yes | No | Zero-signature: your backend is the sole identity source. Self-serve: pick it in the portal's auth-mode selector. | ## wallet-only No backend. The user connects a wallet and signs a challenge; the iframe handles the flow. Just mount: ```ts const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', mode: 'single', }); await chat.mount(); ``` ## app-trusted + wallet Your backend mints a short-lived token for the connected wallet, and the user signs a challenge to prove they own it. This is the default for the public API. ### 1. Mint tokens on your backend The token is a JWT signed with your embed's **app secret** (from the portal), with the user's wallet as `sub` and your `appId` as `app_id`. Keep the secret server-side only. ```ts // POST /api/cherry-embed-token - your server import jwt from 'jsonwebtoken'; import { randomUUID } from 'node:crypto'; const token = jwt.sign( { sub: walletAddress, app_id: process.env.CHERRY_APP_ID }, process.env.CHERRY_APP_SECRET, { algorithm: 'HS256', expiresIn: '5m', jwtid: randomUUID() }, ); ``` > **Note:** A complete, runnable token server lives in the public [`chat-embed-sdk`](https://github.com/cherrydotfun/chat-embed-sdk/tree/main/example/app-trusted+wallet) repo, under `example/app-trusted+wallet` (its `server.js`). ### 2. Mount with the token + a sign handler Fetch a token, then mount. Register `signChallengeHandler` **before** `mount()`: the iframe calls it to get the user's signature. ```ts const provider = window.phantom?.solana; const { publicKey } = await provider.connect(); const walletAddress = publicKey.toString(); const { token } = await fetch('/api/cherry-embed-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress }), }).then((r) => r.json()); const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', token, walletAddress, signChallengeHandler: async (message) => { // message: Uint8Array. Sign the bytes as-is, don't re-encode them const { signature } = await provider.signMessage(message, 'utf8'); return signature; // Uint8Array }, }); await chat.mount(); ``` ### 3. Session renewal is automatic The embed token is short-lived (~5 min) by design: mint it fresh right before `mount()`, don't cache it. After the initial exchange the iframe holds a short-lived Cherry session and silently re-establishes it from a rotating refresh token whenever the page (re)loads, so there's nothing for you to refresh on a timer. If you ever need to force a fresh exchange (e.g. the user switches accounts in your app), mint a new token and call `chat.setToken(token)`. ## app-trusted (zero-signature) The same backend-minted token as above, but with no wallet signature at all: your backend is the sole source of identity truth, and the user never connects a wallet or signs anything on your page. This mode is self-serve: pick it in the portal's auth-mode selector when you configure the embed. ### How it works 1. Your backend authenticates the user through its own login session. 2. It mints the exact same JWT as in `app-trusted+wallet`: `sub` is the user's wallet, `app_id` is your embed ID, HS256 with the app secret, ~5 min expiry, unique `jwtid`. 3. The page fetches the token and mounts with it. No `walletAddress`, no `signChallengeHandler`, no wallet UI: the token's `sub` claim carries the identity. ```ts // Your backend derives the wallet from ITS OWN session. Never accept a // wallet address from the request body: with no signature to check, a // body-supplied address would let any client impersonate any wallet. const { token } = await fetch('/api/cherry-embed-token', { method: 'POST' }) .then((r) => r.json()); const chat = new CherryEmbed({ appId: 'YOUR_EMBED_ID', container: '#cherry-chat', roomId: 'YOUR_ROOM_ID', token, }); await chat.mount(); ``` ### The trust model In the wallet-backed modes the user proves wallet ownership with an Ed25519 signature that Cherry verifies. Here Cherry only verifies that the token was signed with your app secret; whatever `sub` your backend asserts is accepted as-is. Cherry cannot tell a real wallet owner from an impersonated one, which has two consequences: - `sub` must come only from your authenticated session, never from client input (request body, query, headers you don't control). - The mode runs with server-side restrictions, below. ### Server-side restrictions - **Allowed rooms are mandatory and fail-closed.** The chat works only in the rooms allowed for your embed; any other room returns `403`. - **Message rate limits** apply (about 20 messages per minute per user by default); exceeding them returns `429`. - **Moderation from the embed is off by default**, governed by the embed's server-side policy. - **Embed sessions live up to 15 minutes** and refresh automatically, so there is nothing to refresh on your side. Treat `403` and `429` responses from these rules as policy, not as bugs in your integration or a broken token. > **Note:** Turn this mode on yourself: open the embed in the portal and pick **App-trusted (zero-signature)** in the auth-mode selector. Make sure the rooms you need are allowed for the embed. ## Security notes - The **app secret** signs tokens: keep it on your server, never ship it to the browser. - Tokens are short-lived by design; mint them fresh right before `mount()`. Session renewal afterwards is automatic. - `walletConnectRequested` fires when a user clicks "connect" in a preview state: re-arm auth by calling `setToken()` and `setWalletAddress()`. (Wallet-backed modes only; it never fires in pure `app-trusted`.) ## Next steps - [Display modes](https://portal.cherry.fun/docs/embed/display-modes.md) · [SDK API reference](https://portal.cherry.fun/docs/embed/api-reference.md) - Full walkthrough: [Guides › Authenticated chat](https://portal.cherry.fun/docs/guides/authenticated-chat.md)