Skip to content

About

Demo site

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Web3 Shoutbox

XMTP GunDB Vite License: MIT

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 Screenshot

What is this?

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.

Features

  • πŸ” 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

Quick Start

  1. Clone the repository

    git clone <repository-url>
    cd web3-shoutbox-platform
  2. Install dependencies

    npm install
  3. Configure environment variables

    cp .env.local.example .env.local
    # Edit .env.local β€” set VITE_XMTP_ENV=dev and
    # VITE_APP_URL=http://localhost:3000
  4. Run the development server

    npm run dev
  5. Open in browser Navigate to http://localhost:3000

Embed the Widget

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 β†’

Tech Stack

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

Architecture Overview

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 β†’

Project Structure

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

Development

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

Environment Variables

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.

Deployment

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 build

CI (.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).

Custom domain or PinMe subdomain

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.dev
  • shoutbox.pinme.dev (contains a dot) is treated as a custom DNS domain and requires you to control the DNS for pinme.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=shoutbox

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

Repository configuration

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.

Manual deploy

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
fi

Other static hosts

Vercel, Netlify, and Cloudflare Pages still work with the same out/ directory:

npx vercel --prod
npx netlify deploy --prod --dir=out

For Cloudflare Pages, use build command npm run build and output directory out.

Documentation

  • 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

License

MIT License β€” see LICENSE for details.

About

Demo site

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages