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 , 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 , 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 labelYour backendUser signsPick it when
wallet-onlyCherry verifies usersnoneyes, in the iframeYou have no backend, or the room is public and anyone with a wallet may post
app-trusted+walletYour app vouches for usersone token routeyes, on your pageYour users are already signed in and you want verified wallet identity. This is the default for production embeds
app-trustedApp-trusted (zero-signature)one token routenoYour 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

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:

<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.

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.

'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:

// 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.

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 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 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.

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 and follow the quickstart , or the authenticated chat guide if your users are already signed in.

Sources

Build on Cherry

Add chat to your dApp, ship a mini app, or run bots and rooms through the API. Self-serve at the developer portal.