Live emoji reactions for slide presentations, powered by Cloudflare Workers and Durable Objects.
Add real-time, audience-driven emoji reactions to any presentation framework. Audience members tap an emoji, and every connected viewer sees the count update instantly over WebSockets.
┌──────────────┐ WebSocket ┌─────────────────────┐
│ Browser A │──────────────▶│ Cloudflare Worker │
│ (presenter) │◀──────────────│ │
└──────────────┘ │ ┌─────────────────┐ │
│ │ Durable Object │ │
┌──────────────┐ WebSocket │ │ (per slide) │ │
│ Browser B │──────────────▶│ │ │ │
│ (audience) │◀──────────────│ │ SQLite storage │ │
└──────────────┘ │ └─────────────────┘ │
└─────────────────────┘
- Each slide gets its own Durable Object instance, identified by the slide ID.
- Clients connect via WebSocket to
/ws/:slideId— the Worker routes the connection to the correct Durable Object. - When a user reacts, the client sends
{ type: "react", emoji: "👍" }over the WebSocket. - The Durable Object increments the count in its SQLite database and broadcasts the update to every connected client.
- On initial connection, the server sends the full reaction counts so late-joiners see the current state.
Rate limiting (10 reactions per 5-second window per client) and emoji validation are handled server-side.
├── reactions-worker/ # Cloudflare Worker backend
│ └── src/
│ ├── index.ts # HTTP router (WebSocket upgrade, REST, CORS)
│ └── reactions-do.ts # Durable Object (WebSocket handling, SQLite storage)
│
└── slide-reactions/ # npm packages (monorepo)
└── packages/
├── slide-reactions/ # Client library + React components
│ └── src/
│ ├── client.ts # Framework-agnostic WebSocket client
│ ├── hooks/useReactions.ts # React hook
│ ├── components/ReactionBubble.tsx # Drop-in React component
│ └── types.ts # Shared TypeScript types
│
└── create-slide-reactions-worker/ # CLI scaffolding tool
├── src/index.ts # npx create script
└── template/ # Worker template files
cd reactions-worker
npm install
npm run dev # Local development on :8787
npm run deploy # Deploy to Cloudflare Workers
The worker exposes:
| Route | Description |
|---|---|
GET /ws/:slideId |
WebSocket upgrade — connects to the Durable Object for that slide |
GET /reactions/:slideId |
REST endpoint — returns current reaction counts as JSON |
GET / or GET /health |
Health check |
Install the client library:
npm install slide-reactions
import { ReactionBubble } from "slide-reactions";
function App() {
return (
<ReactionBubble
serverUrl="wss://your-worker.workers.dev"
slideId="slide-0"
/>
);
}
The <ReactionBubble> renders a floating button that expands into an emoji reaction panel with live counts, sound feedback, and connection status.
import { useReactions } from "slide-reactions";
function CustomReactions({ slideId }: { slideId: string }) {
const { counts, react, isConnected } = useReactions({
serverUrl: "wss://your-worker.workers.dev",
slideId,
});
return (
<div>
{Object.entries(counts).map(([emoji, count]) => (
<button key={emoji} onClick={() => react(emoji)}>
{emoji} {count}
</button>
))}
</div>
);
}
import { SlideReactionsClient } from "slide-reactions/client";
const client = new SlideReactionsClient({
serverUrl: "wss://your-worker.workers.dev",
slideId: "slide-0",
onCountsUpdate: (counts) => console.log(counts),
onReaction: (emoji, count) => console.log(`${emoji}: ${count}`),
});
client.connect();
client.react("👍");
If you want to spin up your own instance of the backend:
npx create-slide-reactions-worker my-reactions
cd my-reactions
npm install
npm run dev
| Variable | Description | Default |
|---|---|---|
ALLOWED_ORIGINS |
Comma-separated list of allowed CORS origins | * |
Set via wrangler.jsonc vars or Cloudflare dashboard secrets.
| Prop / Option | Type | Default | Description |
|---|---|---|---|
serverUrl |
string |
— | WebSocket server URL (required) |
slideId |
string |
— | Current slide identifier (required) |
emojis |
string[] |
["👀", "👍", "❤️", "🤯"] |
Available emoji reactions |
maxReconnectAttempts |
number |
Infinity |
Max auto-reconnect attempts |
All client options above, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
position |
"bottom-right" | "bottom-left" | "top-right" | "top-left" |
"bottom-right" |
Position of the floating bubble |
sound |
boolean |
true |
Play a pop sound on reaction |
className |
string |
— | Additional CSS class |
style |
CSSProperties |
— | Additional inline styles |
- Durable Objects with Hibernation API — WebSocket connections are managed efficiently; the DO hibernates when idle and wakes on incoming messages.
- SQLite storage — Reaction counts persist in the Durable Object's built-in SQLite database. Each emoji is a row with a count.
- Rate limiting — Sliding window (10 reactions per 5 seconds per WebSocket connection) prevents spam.
- Reconnection with exponential backoff — The client automatically reconnects (1s, 2s, 4s, ... up to 30s) on disconnection.
- Slide switching — Calling
changeSlide()or updating theslideIdprop cleanly disconnects from the old slide and connects to the new one, resetting counts.
# Worker
cd reactions-worker
npm install
npm run dev
# Client library (watch mode)
cd slide-reactions
npm install
npm run dev
MIT