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 β set VITE_XMTP_ENV=dev and # VITE_APP_URL=http://localhost:3000
-
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.
π Full Embed Guide β
| 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. GunDB should be on https if the shoutbox is on https.
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: 60 minutes). This avoids the 250-member cap and eliminates expensive removeMembers MLS operations.
π Full Architecture Doc β
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
βββ scripts/
β βββ copy-spa-routes.mjs # Materialize routes for IPFS (no SPA rewrite)
β βββ gun-relay.mjs # Local GunDB relay
βββ .github/workflows/
β βββ ci.yml # Tests, lint, static export verification
β βββ deploy.yml # Pin `out/` to IPFS with PinMe on main
βββ 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 |
No | β | Accepted for backwards compatibility; unused |
VITE_XMTP_ENV |
Yes | β | XMTP network environment: dev or production |
VITE_APP_URL |
Yes | β | Public base URL where the app is served. A full URL or a bare hostname both work; bare hostnames are normalized to HTTPS (shoutbox.pinme.dev β https://shoutbox.pinme.dev). Use http://localhost:3000 locally. |
VITE_GUN_RELAY_PEERS |
No | http://localhost:8765/gun |
Comma-separated GunDB relay peer URLs; use a hosted HTTPS relay in production |
VITE_SLIDING_WINDOW_MINUTES |
No | 60 |
Duration of each sliding window epoch in minutes |
VITE_SHOUTBOX_BRIDGE_ADDRESSES |
No | β | Optional bridge addresses seated in every room |
VITE_XMTP_MLS_DIAGNOSTICS |
No | false |
Set to true to emit [xmtp-mls-diagnostics-v1] JSON in production builds |
VITE_* values are embedded in the public browser bundle. Never put private keys,
API tokens, or other credentials in them.
Unit tests do not require a .env.local: vitest.config.ts supplies safe test
defaults for VITE_XMTP_ENV and VITE_APP_URL.
The shoutbox is a static site β out/ is the whole application. Pushes to main pin
that directory to IPFS with PinMe via .github/workflows/deploy.yml, the same deployment
pattern as the redhaven and fairhaven repos. There is no server.
npm run buildCI (.github/workflows/ci.yml) runs unit tests, lint, and a production build on pushes
and pull requests to main and develop.
The build also materializes out/rooms/, out/settings/, and out/embed/shoutbox/.
IPFS gateways have no SPA rewrite, so those copies are what keep deep links and the
embeddable widget working after a PinMe deploy. public/_redirects and public/_headers
only apply on hosts that support them (Netlify, Cloudflare Pages).
VITE_APP_URL is the public base URL shown by the app; it does not control where
PinMe publishes the build. PinMe treats the PINME_DOMAIN value differently depending
on whether it contains a dot:
shoutbox(no dot) creates a PinMe subdomain:https://shoutbox.pinme.devshoutbox.pinme.dev(contains a dot) is treated as a custom DNS domain and requires you to control the DNS forpinme.dev
Since pinme.dev is PinMe's root domain, use the subdomain form to publish at
shoutbox.pinme.dev:
VITE_APP_URL=shoutbox.pinme.dev
PINME_DOMAIN=shoutboxVITE_APP_URL accepts a bare hostname and normalizes it to
https://shoutbox.pinme.dev. If PINME_DOMAIN is omitted, PinMe deploys to its
default gateway URL, and VITE_APP_URL should match that URL instead.
For a domain you own (for example chat.example.com), use the full hostname as
PINME_DOMAIN and configure the DNS records PinMe provides.
Add configuration under Settings β Secrets and variables β Actions. Use
repository variables for VITE_* values and PINME_DOMAIN β they are public
build configuration, not secrets. The workflows do not declare a GitHub
environment, so environment-scoped variables are not available.
Required secret:
| Secret | Description |
|---|---|
PINME_APPKEY |
PinMe API key (dashboard or pinme show-appkey) |
Variables (the workflows read vars.*, then fall back to secrets.* for backwards
compatibility, then to the defaults below):
| Variable | Default | Description |
|---|---|---|
VITE_XMTP_ENV |
production |
XMTP network (dev or production) |
VITE_APP_URL |
https://web3-shoutbox.pinme.eth.limo/ |
Public base URL. Bare hostnames are normalized to HTTPS. Set this to the same origin configured in PINME_DOMAIN |
VITE_GUN_RELAY_PEERS |
http://localhost:8765/gun |
Comma-separated hosted GunDB relay URLs. Without a hosted relay, visitors cannot see each other |
VITE_SLIDING_WINDOW_MINUTES |
60 |
Sliding window epoch length |
VITE_SHOUTBOX_BRIDGE_ADDRESSES |
β | Optional bridge addresses seated in each room |
PINME_DOMAIN |
β | PinMe subdomain name without a dot (shoutbox β https://shoutbox.pinme.dev), or a full DNS domain you control (chat.example.com). Set together with VITE_APP_URL; omit for the default gateway URL |
VITE_XMTP_MLS_DIAGNOSTICS |
false |
Set to true to emit [xmtp-mls-diagnostics-v1] JSON in production builds; keep off normally |
VITE_WALLETCONNECT_PROJECT_ID is still accepted as a secret but is unused.
CI passes vars.X || secrets.X || <default> to both the test and build steps, so the
pipeline still works when variables are not configured. CI falls back to dev and
http://localhost:3000; the deploy workflow falls back to the production values in the
table. Unit tests use their own safe defaults and never read repository secrets.
npm ci
npm run build
npx pinme@2.0.12 set-appkey "$PINME_APPKEY"
if [ -n "${PINME_DOMAIN:-}" ]; then
npx pinme@2.0.12 upload ./out --domain "$PINME_DOMAIN"
else
npx pinme@2.0.12 upload ./out
fiVercel, Netlify, and Cloudflare Pages still work with the same out/ directory:
npx vercel --prod
npx netlify deploy --prod --dir=outFor Cloudflare Pages, use build command npm run build and 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.
