"Middle Ground" / Jubilee-style live debate stage. One person in the center hot seat, a ring of participants around them, anyone can challenge for the mic, and the crowd can vote someone out if they're not bringing it. Built to be clip-bait for Twitter/TikTok.
- A Host/Speaker starts a debate session and proposes a topic.
- One person sits in the center seat (the "hot seat") — visually placed dead center of the screen.
- Up to N other participants are arranged in a circle/ring around the center seat (think: video tiles arranged radially, not a boring grid).
- Anyone in the room can press SPACE to challenge for the center seat.
- On press, a 3...2...1 countdown runs.
- If multiple people press SPACE, there's a contention/queue mechanic (see below) — first press wins, or a quick draw mechanic, TBD.
- When countdown hits 0, the challenger swaps into the center seat (and the previous center occupant rotates back into the ring, or gets removed depending on game mode).
- The surrounding crowd can flag/downvote the current center speaker.
- If flags cross a threshold (e.g. majority of active ring members, or a raw count), the speaker gets voted out of the center seat automatically.
- This is designed to be fast, chaotic, and watchable — the kind of thing that gets clipped and shared.
- Jubilee-style content already performs insanely well on YouTube/Twitter/TikTok.
- This format is inherently shareable — clip-worthy moments happen constantly (call-outs, vote-outs, mic-drop moments).
- Low friction to join (press space, talk), high friction to leave gracefully (you get voted off) = great drama engine.
- If we get even one creator with an audience (a "Dean," a debate-bro YouTuber, a podcast host) to run a session, it could spread organically — the format itself is the marketing.
| Mechanic | Notes / Open Questions |
|---|---|
| Center seat swap | Need a clean transition — fade/zoom animation, audio crossfade, not jarring |
| SPACE to challenge | Need debounce + anti-spam (can't mash space). Maybe a per-user cooldown after losing a challenge. |
| Countdown (3..2..1) | Synced across all clients — server-authoritative countdown, not client-local timers (avoid desync/cheating) |
| Simultaneous SPACE presses | Decide: first-server-timestamp wins? Random pick among first N ms? Queue system (next challenger auto-loads after current swap)? |
| Flag / vote-out | Threshold logic: % of active ring members vs raw count. Cooldown so center speaker isn't insta-voted in 2 seconds. Maybe minimum "grace period" (e.g. 30s) before votes count. |
| Ring layout | Circular video tile arrangement, responsive to ring size (6 people vs 19 people circle differently) |
| Spectators vs Ring members | Are there people who just watch (no mic, no vote) vs active ring participants (mic-eligible, can vote)? Probably yes — spectators >> active ring slots. |
| Moderation | Who can mute/kick outright (host powers) vs democratic vote-out? Need both. |
- LiveKit — handles all real-time audio/video (the actual debate audio, video tiles, screen presence)
- Express.js — backend for room orchestration, auth, vote tallying, countdown state machine, LiveKit token issuance
- Socket.io (or LiveKit data channels) — for low-latency game-state events: countdown ticks, vote counts, seat swaps, flags
- Postgres/Redis — Redis is probably essential here for ephemeral, high-frequency state (votes, queue, countdown) since this is much more "live game state" than "persistent chat app"
- Frontend — React, with a custom radial/circular layout renderer for video tiles (this is the most visually distinctive piece — worth getting right)
Client (React)
│
├── LiveKit SDK ──────────────► LiveKit Server (audio/video tiles)
│
└── Socket.io ────────────────► Express + Redis
├── Room/session state machine
├── Countdown authority (server clock)
├── Vote/flag tally + threshold logic
├── Seat assignment (center vs ring vs spectator)
└── LiveKit token + permission grants (mic on/off based on seat)
LiveKit gives you the audio/video pipes. It does not give you:
- Game-state logic (who's in the center, vote counts, countdown sync)
- Seat-based permission switching (only center seat can publish audio? or can ring members talk too?)
- Vote-out thresholds and cooldowns
So Express + Redis becomes the referee — it decides who gets a LiveKit token with canPublish: true for the center seat, and revokes/reissues that permission live as seats swap.
States per room:
- WAITING (host hasn't started yet)
- TOPIC_PROPOSED (host announces topic)
- ACTIVE_DEBATE (center seat occupied, ring can challenge/flag)
- COUNTDOWN (someone pressed SPACE, 3..2..1 running)
- SEAT_SWAP (countdown hit 0, swapping center occupant)
- VOTE_OUT (flag threshold hit, removing center occupant)
Each transition is server-authoritative — broadcast via Socket.io to all clients so animations/timers stay in sync. Never trust client-side countdowns for anything that affects game state.
To ship something testable fast, cut scope to:
- One room, one topic, fixed ring size (e.g. 8 ring slots)
- Center seat + ring, no spectator tier yet
- SPACE to challenge → server countdown → swap (first-press-wins, no fancy queue)
- Flag button → raw count threshold (e.g. 5 flags) → auto vote-out
- Basic radial video layout (CSS, not canvas/WebGL — keep it simple first)
- Host controls: start topic, force-kick, end session
Punt to v1+: spectator mode, queue system for simultaneous challenges, clip/export tool (huge for virality — letting people clip a moment straight to a shareable video is probably worth its own workstream), persistent accounts/history, multiple simultaneous rooms/discovery, mobile layout.
- What happens to the person voted out — do they go back in the ring, or are they removed from the session entirely (for that topic)?
- Can ring members talk at all, or only the center seat has a live mic (others muted until they win the seat)?
- Is there a time limit on how long someone can hold center seat regardless of votes?
- Anonymous join vs account required? (Lower friction = more virality, but also more trolling)
- Clip/export feature — this might be the single highest-leverage feature for actually getting shared on Twitter
- Max ring size — visually, how many tiles can a circle hold before it looks bad / how do we degrade gracefully at scale (the "19 people" case)?
Foundation for multi-room voice/video chat:
- Ask user's display name (stored locally)
- Create a room with configurable capacity
- Shareable invite link (
/room/:id) - Join via link and talk (LiveKit audio/video)
- Multiple rooms can exist simultaneously
| Requirement | Notes |
|---|---|
| Node.js 18+ | node -v |
| Docker | For LiveKit only (docker compose) |
| npm | Installed with Node |
Clone and install once:
git clone <repo-url> circle
cd circle
npm install
cp .env.example server/.env # optional — defaults work for local devUse this to test in two browser tabs on the same machine.
# Terminal 1 — LiveKit (use sudo if Docker needs it)
npm run livekit
# Terminal 2 — app
npm run dev- Open http://localhost:5173
- Enter your display name
- Create a room → copy the invite link
- Open the invite link in a second tab (use a different name)
- On both tabs: click Enable camera & mic
- You should see video tiles and hear audio between tabs
Browsers block camera/mic on http://10.x.x.x — you need HTTPS on your LAN IP.
npm run dev:lanThis script will:
- Generate self-signed TLS certs (with your Wi‑Fi IP in the SAN)
- Write
livekit.yamlwith your LAN IP for WebRTC (rtc.node_ip) - Recreate the LiveKit Docker container
- Start the dev server over HTTPS
Use only the URL printed in the terminal, e.g.:
https://10.0.0.238:5173
| Do | Don't |
|---|---|
Open the printed https://10.x.x.x:5173 on laptop and phone |
Use http:// on a LAN IP |
| Accept the certificate warning on each device | Use 172.x.x.x URLs (Docker bridge, not Wi‑Fi) |
| Join the same room and click Enable camera & mic on both | Open :7880 directly in the browser |
Firewall (if media doesn't flow — signaling works but no audio/video):
sudo ufw allow 5173/tcp # App HTTPS + LiveKit signaling (proxied)
sudo ufw allow 7882/udp # LiveKit ICE
sudo ufw allow 50000:60000/udp # LiveKit media portsWebRTC media goes directly to LiveKit on UDP — it does not pass through the Vite proxy.
| Command | What it does |
|---|---|
npm run dev |
Start server + client (localhost HTTP) |
npm run dev:lan |
HTTPS on LAN IP + regenerate certs & LiveKit config |
npm run livekit |
Write livekit.yaml and start/recreate LiveKit container |
npm run gen-certs |
Regenerate self-signed certs in certs/ |
npm run gen-livekit |
Regenerate livekit.yaml only (no Docker) |
Check LiveKit is running:
curl http://127.0.0.1:7880 # should return HTTP 200
sudo docker compose logs livekit --tail 20
cat livekit.yaml | grep node_ip # LAN IP when using dev:lan, 127.0.0.1 for laptop-only"Connecting to voice/video…" forever
- LiveKit isn't running →
npm run livekit - Wrong URL → use
localhost:5173or thehttps://10.x.x.x:5173URL fromdev:lan, not:7880
Camera light on but no video / no audio
- LiveKit ICE is using the wrong IP → run
npm run dev:lan(ornpm run livekitwithPUBLIC_HOST=10.x.x.x) - Firewall blocking UDP → open
7882/udpand50000:60000/udp - Only one participant → voice needs two people in the room; you should still see your own video after enabling camera
yaml: field tls not found in Docker logs
- Old container with a stale config →
sudo docker compose down livekit && npm run livekit
Wrong address warning (172.x.x.x)
- You opened a Docker bridge IP. Use the
https://10.x.x.x:5173URL fromnpm run dev:lan.
getUserMedia / camera not available
- On LAN you must use HTTPS (
npm run dev:lan), not plainhttp://10.x.x.x.
- Lock MVP scope (above) and ring size
- Build the server-authoritative state machine in Express + Redis
- Prototype the radial video layout in isolation (this is the part most likely to take longer than expected)
- Wire LiveKit token permissions to seat state
- Internal test with a small group before any public/Twitter push