DEV Community

Fah Swe
Fah Swe

Posted on

Designing a 12-Seat Voice Room: WebRTC/SFU, Server-Authoritative Coins, and Moderation

Social voice rooms look simple: a grid of 10–12 mic seats, a chat strip, some gift animations. Underneath, they combine three hard problems — real-time media, money, and trust & safety. Get any one wrong and the room either lags, leaks money, or turns toxic.

This post walks through a practical design for a multi-seat voice room: how to route audio, how to keep a virtual-coin economy honest, and how to build moderation into the architecture instead of bolting it on.

1. Media: why an SFU, not mesh or MCU

With WebRTC you have three topologies:

  • Mesh (P2P): every participant sends to every other participant. With 12 speakers, each client uploads 11 streams. Mobile uplinks and batteries give up fast. Fine for 1:1 or 3-person calls, not for rooms.
  • MCU: the server decodes and mixes all audio into one stream per listener. Clients are cheap, but the server burns CPU on decode/mix/encode and you lose per-speaker control (volume, mute indicators, active-speaker detection on the client).
  • SFU (Selective Forwarding Unit): each client uploads one stream; the server forwards packets to subscribers without decoding. This is the sweet spot for voice rooms. Open-source options include LiveKit, mediasoup, and Janus.

A voice room naturally splits participants into publishers (people on mic seats) and subscribers (the audience). Only seated users get publish permission; everyone else is receive-only. That means a room with 12 seats and hundreds of listeners still has at most 12 upstream audio tracks.

Practical tips:

  • Opus with DTX (discontinuous transmission) cuts bandwidth when a speaker is silent, which is most of the time in a 12-seat room.
  • Audio levels / active speaker events from the SFU drive the "speaking ring" animation around seats; don't compute it on each client from raw audio.
  • Tokens, not trust: issue short-lived access tokens from your backend with explicit grants (canPublish, canSubscribe, room name, identity). When a user takes a seat, mint a new token or update permissions server-side. Never let the client decide it can publish.
  • Scale by room, not by user: pin a room to one SFU node where possible; for very large audiences, cascade or relay to edge nodes.

2. Seats are state — keep them on the server

The seat grid is shared state: who is on seat 3, is seat 7 locked, is seat 5 muted by the host? Treat it like a tiny multiplayer game:

RoomState {
  roomId, ownerId, mode, passwordHash?,
  seats: [ { index, userId|null, locked, mutedByHost } x 12 ],
  admins: [userId], version
}
Enter fullscreen mode Exit fullscreen mode
  • All seat changes (take, leave, lock, mute, kick) go through the backend as commands.
  • The backend validates permissions, applies the change atomically (e.g. a Redis transaction or a row lock), bumps version, and broadcasts the new state over your signaling/data channel.
  • Clients render state; they never mutate it locally except for optimistic UI that gets reconciled.
  • When a seat is lost, revoke publish permission on the SFU as well — UI state and media permissions must agree, or a kicked user can keep talking.

3. Coins and gifts: server-authoritative, always

Gifting is where money lives, so the rule is absolute: the client never computes balances. It sends intents; the server decides.

A safe gift flow:

  1. Client sends SendGift { giftId, qty, toUserId, roomId, idempotencyKey }.
  2. Server looks up the gift price from its own catalog (never trust a price from the client).
  3. In one database transaction: check the sender's balance, debit sender, credit receiver (often in a different unit, e.g. coins → "diamonds" or points), and write immutable ledger rows.
  4. Commit, then publish a GiftSent event for animations and leaderboard updates.

Design details that save you later:

  • Double-entry ledger: every movement has a debit and a credit row. Balances are derived or cached, but the ledger is the source of truth. Auditing and dispute handling become queries, not archaeology.
  • Idempotency keys: mobile networks retry. Without an idempotency key, a flaky connection turns one gift into three.
  • Integer minor units: store amounts as integers; no floats.
  • Leaderboards from events: hourly/daily/weekly rankings can be Redis sorted sets fed by GiftSent events, rebuilt from the ledger if needed. Keep them eventually consistent — the ledger stays strongly consistent.
  • Payment webhooks: coin purchases should be credited only after a verified payment-provider webhook or receipt validation, not when the client says "payment succeeded."
  • Rate limits and anomaly checks: cap gifts per second per user and flag unusual patterns (new account + large transfers) for review.

4. Moderation as a first-class feature

Voice is harder to moderate than text because it is ephemeral. Build layers:

  • Role model: owner → room admins → seated speakers → listeners. Encode it in the token grants and in the backend command checks.
  • Host tools: mute seat, remove from seat, kick from room, lock seat, password-protect room, toggle chat or gift effects. Each action is a server command that updates both room state and SFU permissions.
  • Text chat filtering: run messages through a filter service before broadcast; let listeners filter the chat view (all / messages / gifts) so gift spam doesn't bury conversation.
  • Reporting + audit log: every moderation action and report is written with actor, target, room, and timestamp. That log is how support resolves disputes.
  • Platform-level controls: global bans, device/account risk signals, and visible safety banners in rooms.
  • Policy and legal fit: if the room embeds mini-games or virtual currency, check app store rules and local regulations for each market before you ship.

5. A reference layout

Mobile app ──(WebRTC audio)──> SFU (LiveKit/mediasoup)
     │                            ▲ permissions via server API
     └──(WebSocket commands)──> Room service ──> Redis (room state, leaderboards)
                                   │
                                   └──> Wallet service ──> SQL ledger (transactions)
                                               ▲
                            Payment webhooks ──┘
Enter fullscreen mode Exit fullscreen mode

Keep media, room state, and money in separate services. Media scales horizontally by room; room state needs low-latency pub/sub; the wallet needs strict transactions. Coupling them is how a traffic spike in one room becomes a billing incident.

Wrap-up

A 12-seat voice room is really three systems: an SFU that forwards at most a dozen publishers, a server-owned seat state machine, and a ledger-backed coin economy — all wrapped in moderation that works at the permission level, not just the UI. Get those boundaries right early and the fun parts (themes, games, animations) are much easier to add.


About the author: I work on real-time voice and live streaming products at Fah Swe, a software company in Sylhet, Bangladesh — fahswe.com.

Top comments (1)

Collapse
 
devsupport profile image
Dev Support •

Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support

​‍‍​