# How to add chat to my dApp with wallet login

> Add chat to your dApp in six steps: create an embed, pick a wallet auth mode, install the SDK, mount on a div, theme it, and set the moderation rules.


Search how to add chat to my dApp and most of the answers are years-old build-your-own tutorials: a WebSocket server, a messages table, a moderation backlog you did not plan for. This guide takes the shorter path. Cherry (cherry.fun) is a wallet-to-wallet messenger and community app for crypto, and its Chat Embed SDK drops one Cherry room into your page as an iframe served from embed.cherry.fun, with wallet sign-in, realtime delivery, unread counts, and server-side moderation already in it. Your users' wallets are the accounts, so there is no user table on your side. Six steps, roughly five minutes for the no-backend version.

## Before you start

You need a Solana wallet to sign in to [Cherry Portal](https://portal.cherry.fun), a web app you can serve over http or https (localhost counts), and the origin you will embed from. A wallet connector on your page is optional: only the `app-trusted+wallet` mode needs one.

The package is `@cherrydotfun/chat-embed-sdk`, version 0.1.7 as of 10 September 2026. It ships ESM, CommonJS, and TypeScript types, so there is no `@types` package to add. The SDK and the portal are free.

## Step 1: Create the embed in Cherry Portal

An embed is the unit of configuration: one surface, one config, one secret, one room. Open [portal.cherry.fun](https://portal.cherry.fun), click **Connect Wallet** and approve the signature (Sign In With Solana: nothing goes on-chain, no gas), then **Create Project**. Inside the project, open **Chat embeds** and create an embed.

Two fields matter immediately. The embed's name becomes the visible room title, so write something a visitor should read, and renaming the embed later renames the live chat too. Under **Allowed origins**, add each origin you will load from, scheme included: `http://localhost:3000` for development and your production URL. Then make sure the embed is enabled.

You know it worked when the embed's **Install** tab shows a snippet with your `appId` and room id already filled in. Copy it from there: the embed creates its own room in Cherry, and the snippet points at that room.

## Step 2: Pick an auth mode

The auth mode decides who vouches for each visitor, and the config you write differs for each. The portal's selector asks "Who signs your users in?" and labels the options in plain language, which does not match the mode names you write in config, so here are both.

| Mode (config) | Portal label | Your backend | User signs | Pick it when |
|---|---|---|---|---|
| `wallet-only` | Cherry verifies users | none | yes, in the iframe | You have no backend, or the room is public and anyone with a wallet may post |
| `app-trusted+wallet` | Your app vouches for users | one token route | yes, on your page | Your users are already signed in and you want verified wallet identity. This is the default for production embeds |
| `app-trusted` | App-trusted (zero-signature) | one token route | no | Your login is the only identity source and you want no wallet prompt at all |

The zero-signature mode trades a proof for convenience: Cherry checks only that the token was signed with your app secret and accepts whatever wallet your backend asserts, so it runs with server-side restrictions. Rooms are an allowlist that fails closed (anything else returns `403`), messages are rate limited to about 20 per minute per user by default (`429` past that), and moderation from inside the embed is off unless the embed's policy turns it on. Derive the wallet from your own session, never from the request body.

## Step 3: Install the SDK

```bash
npm install @cherrydotfun/chat-embed-sdk
```

For a plain HTML page with no build step, load the global build from jsDelivr instead and pin the version:

```html
<div id="cherry-chat" style="height: 600px"></div>

<script src="https://cdn.jsdelivr.net/npm/@cherrydotfun/chat-embed-sdk@0.1.7/dist/index.global.js"></script>
<script>
  new window.CherryEmbedSDK.CherryEmbed({
    appId: 'YOUR_EMBED_ID',
    container: '#cherry-chat',
    roomId: 'YOUR_ROOM_ID',
    theme: { mode: 'dark', primaryColor: '#FF5BA8' },
  }).mount();
</script>
```

## Step 4: Mount the widget

In `wallet-only` mode the whole integration is a container and a constructor. `roomId` is required because `single` is the only room mode available today.

```ts
import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk';

const chat = new CherryEmbed({
  appId: 'YOUR_EMBED_ID',
  container: '#cherry-chat',
  roomId: 'YOUR_ROOM_ID',
  mode: 'single',
  theme: { mode: 'dark', primaryColor: '#FF5BA8' },
});

await chat.mount();
```

In React the mount belongs in an effect, with `destroy()` in the cleanup. One detail is worth copying exactly: assign the instance to a local variable synchronously, before the first `await`. StrictMode double-invokes effects in development, and if cleanup runs while `mount()` is still waiting on the iframe handshake, a cleanup that reads a ref set later cannot destroy the first iframe and a second one stacks beside it.

```tsx
'use client';
import { useEffect, useRef } from 'react';
import { CherryEmbed } from '@cherrydotfun/chat-embed-sdk';

export function CherryChat() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = ref.current;
    if (!container) return;
    let cancelled = false;
    let chat: CherryEmbed | null = null;

    (async () => {
      chat = new CherryEmbed({
        appId: process.env.NEXT_PUBLIC_CHERRY_APP_ID!,
        container,
        roomId: process.env.NEXT_PUBLIC_CHERRY_ROOM_ID!,
        mode: 'single',
      });
      await chat.mount();
      if (cancelled) return;
      chat.on('ready', () => console.log('chat ready'));
    })();

    return () => {
      cancelled = true;
      chat?.destroy();
    };
  }, []);

  return <div ref={ref} style={{ height: 600 }} />;
}
```

The `'use client'` directive is there because the SDK touches `document`, an iframe, and `window.postMessage`, so it cannot run during a server render. Beyond that, any modern bundler works.

For `app-trusted+wallet`, add a backend route that mints a short-lived HS256 JWT with the user's wallet as `sub`, your embed id as `app_id`, and a unique `jwtid`, signed with the embed's app secret:

```ts
// POST /api/cherry-embed-token
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() },
);
```

Then mount with `token`, `walletAddress`, and a `signChallengeHandler` in the config. Register the handler in the config rather than after mount: the iframe calls it during the handshake, and `onSignChallenge()` throws if you call it before `mount()`. The handler receives a `Uint8Array` and must sign those bytes as they are, with no re-encoding.

You know it worked when the room renders with its real title and message history, and pressing send brings up your wallet's signature prompt.

## Step 5: Theme it with seed colors

Pass two to four seed colors and a mode, and the engine derives the rest of the palette, around 60 CSS variables, with contrast floors it will not break: body text at 4.5:1, incoming text at 4.5:1, the send icon at 3:1.

```ts
theme: {
  mode: 'dark',
  primaryColor: '#9162FF',      // your messages, the send button
  backgroundColor: '#0D0416',   // surfaces, chrome, text polarity
  accentColor: '#FF57C1',       // links and mentions
}
```

`primaryColor` plus `backgroundColor` is the useful minimum. Any derived slot can be overridden, but composing all 40 settable slots by hand tends to read as a tint over the default theme. The [live builder](https://cherry.fun/chat-embed-example/) previews the result with no signup and copies out a matching config. One gotcha: the sanitizer takes only hex, `rgb(a)`, `hsl(a)`, and strict `linear-gradient` values and silently drops the rest, so a color that "does nothing" is usually a rejected value, not the wrong key.

## Step 6: Set the moderation rules

A new embed allows everything, so turn on what you need in the embed's **Moderation** tab and press **Save**. The rules run on Cherry's servers, which is why a visitor cannot get past them by editing your client code.

- Minimum wallet age: 0 to 365 days, measured from the wallet's first on-chain transaction. A wallet with no transactions has an age of zero, so any non-zero threshold blocks fresh throwaways.
- Blocked words: up to 200 entries of 1 to 100 characters, matched case-insensitively. A hit is shadow-dropped, so the sender still sees their own message and gets no signal to retry.
- Links: allow all, an allowlist of up to 100 bare hosts, or block all. An empty allowlist blocks every link.
- Images and GIFs: two independent toggles, both on by default.

Cherry's wider [anti-spam layers](https://cherry.fun/learn/features/anti-spam/) sit behind these, and because the embed's room is a real Cherry community, the same gating applies to it in the Cherry app.

## Troubleshooting

- **The widget returns `401` and nothing renders.** The origin is missing from **Allowed origins**, or the embed is disabled. Add your dev and production origins, and enable the embed.
- **The iframe is blocked by your Content-Security-Policy.** Allow `frame-src https://embed.cherry.fun`.
- **Auth fails a few minutes after page load.** The embed token expires in about five minutes by design. Mint it fresh right before `mount()` and never cache it; after the first exchange the iframe keeps its session alive from a rotating refresh token. On an account switch, mint a new token and call `chat.setToken(token)`.
- **The signature is rejected.** `signChallengeHandler` re-encoded the challenge. Sign the `Uint8Array` you were handed and return a `Uint8Array`.
- **Two chat panels appear in development.** The React cleanup missed an instance that was still mid-mount. See the local-variable pattern in step 4.
- **An `error` event never fires.** The `error` event is reserved and not currently emitted. Catch the rejection from `mount()` instead.
- **In `app-trusted` mode a room returns `403` or a user gets `429`.** Both are policy, not bugs: the room allowlist fails closed and the per-user message rate limit is about 20 per minute by default.

## Related guides

- [Embeddable chat on Cherry](https://cherry.fun/learn/features/embeddable-chat/): the full SDK surface, from display modes to host identity and mobile WebViews.
- [Cherry Portal for developers](https://cherry.fun/learn/guides/cherry-portal-for-developers/): projects, keys, and team access.
- [The Cherry API and bots](https://cherry.fun/learn/features/cherry-api-and-bots/): create a room per match or order from your backend, then point an embed at it.
- [Chat for web3 games](https://cherry.fun/learn/guides/chat-for-web3-games/): the same pieces applied to lobbies and matches.
- [Token-gated chat](https://cherry.fun/learn/features/token-gated-chat/): how a room checks holdings on-chain, which is what makes an embed holders-only.
- [Wallet identity on Cherry](https://cherry.fun/learn/features/wallet-identity/): the names, avatars, and badges your visitors bring with them.

## FAQ

**Can I add chat to my dApp without a backend?** Yes, in `wallet-only` mode. The embed needs an `appId` and a room id; visitors connect their own wallet and sign a challenge inside the iframe, and Cherry runs the session. There is no token endpoint to write.

**How do I add chat to a website with wallet login?** Create an embed in Cherry Portal, add your origin to **Allowed origins**, install `@cherrydotfun/chat-embed-sdk` or load it from jsDelivr, and mount `CherryEmbed` on a div. Wallet login is part of the widget: the visitor's Solana wallet signs a challenge, and that signature is the login.

**How do I embed a chat room in React?** Mount inside a `useEffect` with a ref as the container and call `destroy()` in the cleanup, assigning the instance to a local variable before the first `await`. Skip that and StrictMode's double-invoke leaves you two stacked iframes in development.

**Does the Chat Embed SDK work in Next.js?** Yes, in a component that renders in the browser: the SDK uses `document`, an iframe, and `window.postMessage`, so the mounting component carries the `'use client'` directive. Any modern bundler works, Vite and Webpack included.

**Can I add the chat with a script tag instead of npm?** Yes. The jsDelivr build exposes `window.CherryEmbedSDK` with the same `CherryEmbed` constructor. Pin the version in the URL so a future release cannot change behavior under a page that has no lockfile.

Create your embed at [portal.cherry.fun](https://portal.cherry.fun) and follow the [quickstart](https://portal.cherry.fun/docs/quickstart), or the [authenticated chat guide](https://portal.cherry.fun/docs/guides/authenticated-chat) if your users are already signed in.

## Sources

- [Chat Embed SDK quickstart](https://portal.cherry.fun/docs/quickstart)
- [Chat Embed SDK installation](https://portal.cherry.fun/docs/embed/installation)
- [Chat Embed SDK configuration options](https://portal.cherry.fun/docs/embed/configuration)
- [Chat Embed SDK authentication modes](https://portal.cherry.fun/docs/embed/authentication)
- [Guide: authenticated chat with your users' wallets](https://portal.cherry.fun/docs/guides/authenticated-chat)
- [Chat Embed SDK theming](https://portal.cherry.fun/docs/embed/theming)
- [Per-embed moderation rules](https://portal.cherry.fun/docs/embed/moderation)
- [Cherry developer support and common errors](https://portal.cherry.fun/docs/support)
- [@cherrydotfun/chat-embed-sdk on npm](https://www.npmjs.com/package/@cherrydotfun/chat-embed-sdk)
- [Chat Embed SDK source and examples on GitHub](https://github.com/cherrydotfun/chat-embed-sdk)

