A decentralized, embeddable shoutbox widget powered by XMTP for end-to-end encrypted messaging and GunDB for real-time presence. No backend server required — the entire system runs in the browser.
Web3 Shoutbox is a real-time chat widget that any website can embed via an <iframe>. Identity is anchored to crypto wallets, all messages are end-to-end encrypted via XMTP's MLS protocol, and presence (who is online) is tracked peer-to-peer through GunDB. Zero server infrastructure required — just static hosting.
- 🔐 End-to-end encryption — MLS protocol (RFC 9420) via XMTP, mandatory on all messages
- 👛 Wallet-based identity — connect with MetaMask, Rainbow, Coinbase, or any WalletConnect wallet
- 👥 Real-time presence — see who is online via GunDB CRDT-based peer-to-peer sync
- 📦 Embeddable widget — drop an
<iframe>on any website with a single line of HTML - 🔄 Sliding window groups — XMTP groups rotate on a schedule, naturally resetting member counts
- 🏗️ No backend — fully JAMstack, deploy to any static host
- 🗳️ Deterministic leader election — client-side, coordination-free group creation
- 🌙 Dark mode — full light/dark theme support
- 📱 Mobile responsive — works down to 280px width
- 🔔 Toast notifications — real-time feedback for connections, errors, and transitions
-
Clone the repository
git clone <repository-url> cd web3-shoutbox-platform
-
Install dependencies
npm install
-
Configure environment variables
cp .env.local.example .env.local # Edit .env.local — you need a WalletConnect Project ID at minimum -
Run the development server
npm run dev
-
Open in browser Navigate to http://localhost:3000
Add this to any website to embed the shoutbox:
<iframe
src="https://your-shoutbox-domain.com/embed/shoutbox"
width="400"
height="600"
frameborder="0"
style="border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.12);"
allow="clipboard-write">
</iframe>The widget auto-detects the parent page URL as the chat room. Pass ?room=my-room&theme=dark for customization.
| Technology | Version | Purpose |
|---|---|---|
| Vite | 7.x | Build tool & dev server |
| React | 19.x | UI library |
| TypeScript | 5.x | Type safety |
| Tailwind CSS | 4.x | Styling |
| XMTP browser-sdk | 7.x | E2E encrypted messaging (MLS) |
| GunDB | 0.2020.x | CRDT-based real-time presence |
| wagmi | 3.x | Ethereum React hooks |
| viem | 2.x | Ethereum library |
| @reown/appkit | 1.x | Wallet connection (WalletConnect) |
| Zustand | 5.x | State management |
| Zod | 4.x | Runtime validation |
| Sonner | 2.x | Toast notifications |
The shoutbox is a dual-protocol, browser-only system:
- XMTP handles encrypted message transport — messages are signed with wallet keys and encrypted via MLS
- GunDB handles ephemeral presence — who is currently on the page, tracked via heartbeat TTL
These layers are intentionally decoupled. GunDB owns the UI roster; XMTP owns message delivery. Presence changes do not trigger XMTP group mutations.
Groups use a sliding window model — instead of one permanent group per URL, groups rotate on a time-based schedule (default: 5 minutes). This avoids the 250-member cap and eliminates expensive removeMembers MLS operations.
web3-shoutbox-platform/
├── src/
│ ├── components/
│ │ ├── auth/ # ConnectWallet button
│ │ ├── chat/ # ChatContainer, MessageList, MessageBubble, MessageInput
│ │ ├── layout/ # AppLayout, Header
│ │ ├── presence/ # PresencePanel, UserAvatar
│ │ ├── providers/ # Web3Provider, XmtpProvider, GunProvider, ThemeProvider
│ │ ├── ui/ # Skeleton, XmtpStepIndicator
│ │ └── ErrorBoundary.tsx # Top-level error boundary
│ ├── config/
│ │ └── env.ts # Zod-validated environment variables
│ ├── hooks/
│ │ ├── useEmbed.ts # Embed detection, PostMessage API, auto-resize
│ │ ├── useGroupLifecycle.ts # Window management, leader election orchestration
│ │ ├── useLeaderElection.ts # Deterministic leader computation
│ │ ├── useOnlineUsers.ts # Presence subscription → OnlineUser[]
│ │ ├── usePresence.ts # Join/leave room, heartbeat lifecycle
│ │ ├── useShoutboxRoom.ts # Unified room hook (presence + messaging + groups)
│ │ ├── useXmtpClient.ts # XMTP client state from provider
│ │ └── useXmtpConversation.ts # Group message send/receive
│ ├── lib/
│ │ ├── embed-messaging.ts # PostMessage protocol, auto-resize, config parsing
│ │ ├── group-lifecycle.ts # GunDB group read/write/subscribe
│ │ ├── gun.ts # GunDB singleton instance
│ │ ├── gun-presence.ts # Low-level presence read/write
│ │ ├── leader-election.ts # Deterministic leader election algorithm
│ │ ├── retry.ts # Generic retry with exponential backoff
│ │ ├── url-utils.ts # URL normalization + SHA-256 room keys
│ │ ├── utils.ts # Tailwind cn() helper
│ │ └── xmtp.ts # XMTP client factory
│ ├── pages/
│ │ ├── embed/
│ │ │ └── EmbedShoutboxPage.tsx # Compact embed widget page
│ │ ├── NotFoundPage.tsx
│ │ ├── RoomBrowserPage.tsx
│ │ ├── SettingsPage.tsx
│ │ └── ShoutboxPage.tsx # Main standalone shoutbox page
│ ├── services/
│ │ ├── groupLifecycleService.ts # Group creation, discovery, failover
│ │ ├── messagingService.ts # XMTP send/receive with retry
│ │ └── presenceService.ts # GunDB presence join/leave/heartbeat
│ ├── stores/
│ │ ├── authStore.ts # Wallet connection state
│ │ ├── chatStore.ts # Messages and chat UI state
│ │ └── presenceStore.ts # Online users state
│ ├── types/
│ │ ├── embed.ts # PostMessage event/command types
│ │ ├── errors.ts # Typed error classes + error classifiers
│ │ ├── group.ts # GroupWindow, GroupState
│ │ ├── gun.d.ts # GunDB type declarations
│ │ ├── message.ts # ShoutboxMessage
│ │ ├── presence.ts # PresenceRecord, OnlineUser
│ │ └── result.ts # Result<T, E> pattern (ok/err)
│ ├── App.tsx # Router + provider tree
│ ├── globals.css # Tailwind base + custom animations
│ └── main.tsx # Entry point
├── public/
│ ├── test-embed.html # Interactive embed test page
│ ├── _headers # CDN headers for iframe embedding
│ └── _redirects # SPA fallback redirects
├── docs/ # Documentation
│ ├── ARCHITECTURE.md
│ ├── CONTRIBUTING.md
│ ├── EMBED_GUIDE.md
│ └── SETUP.md
├── e2e/ # Playwright E2E test directory
├── .env.local.example # Environment variable template
├── eslint.config.mjs # ESLint configuration
├── index.html # Vite entry HTML
├── package.json # Dependencies & scripts
├── playwright.config.ts # Playwright configuration
├── postcss.config.mjs # PostCSS (Tailwind)
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite configuration
└── vitest.config.ts # Vitest configuration
| Command | Description |
|---|---|
npm run dev |
Start development server on port 3000 |
npm run build |
Type-check and build for production (output: out/) |
npm run preview |
Preview production build locally |
npm run test |
Run unit tests with Vitest |
npm run test:watch |
Run unit tests in watch mode |
npm run test:e2e |
Run Playwright E2E tests |
npm run lint |
Run ESLint |
Create a .env.local file from the template:
cp .env.local.example .env.local| Variable | Required | Default | Description |
|---|---|---|---|
VITE_WALLETCONNECT_PROJECT_ID |
Yes | — | WalletConnect Project ID from Reown Dashboard |
VITE_XMTP_ENV |
Yes | — | XMTP network environment: dev or production |
VITE_APP_URL |
Yes | — | Application base URL (e.g., http://localhost:3000) |
VITE_GUN_RELAY_PEERS |
No | Public relays | Comma-separated GunDB relay peer URLs |
VITE_SLIDING_WINDOW_MINUTES |
No | 5 |
Duration of each sliding window epoch in minutes |
The shoutbox is a static site — build it and deploy the out/ directory to any static host.
npm run buildnpx vercel --prodSet the build output directory to out and add environment variables in the Vercel dashboard.
The public/_redirects file handles SPA routing automatically. Deploy via:
npx netlify deploy --prod --dir=outThe public/_headers file configures iframe embedding headers. Connect your repository in the Cloudflare Pages dashboard with:
- Build command:
npm run build - Build output directory:
out
- Architecture — System design, data flow, and key decisions
- Embed Guide — Third-party integration guide with PostMessage API
- Setup Guide — Detailed environment setup and troubleshooting
- Contributing — Code style, workflow, and PR guidelines
MIT License — see LICENSE for details.
