<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fah Swe</title>
    <description>The latest articles on DEV Community by Fah Swe (@fahswe).</description>
    <link>https://dev.to/fahswe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144841%2Fea3143ee-8a95-4ac4-8a40-d27966c24504.png</url>
      <title>DEV Community: Fah Swe</title>
      <link>https://dev.to/fahswe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fahswe"/>
    <language>en</language>
    <item>
      <title>Designing a 12-Seat Voice Room: WebRTC/SFU, Server-Authoritative Coins, and Moderation</title>
      <dc:creator>Fah Swe</dc:creator>
      <pubDate>Sat, 26 Sep 2026 21:03:38 +0000</pubDate>
      <link>https://dev.to/fahswe/designing-a-12-seat-voice-room-webrtcsfu-server-authoritative-coins-and-moderation-4l0p</link>
      <guid>https://dev.to/fahswe/designing-a-12-seat-voice-room-webrtcsfu-server-authoritative-coins-and-moderation-4l0p</guid>
      <description>&lt;p&gt;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 &amp;amp; safety. Get any one wrong and the room either lags, leaks money, or turns toxic.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Media: why an SFU, not mesh or MCU
&lt;/h2&gt;

&lt;p&gt;With WebRTC you have three topologies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mesh (P2P):&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCU:&lt;/strong&gt; 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).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SFU (Selective Forwarding Unit):&lt;/strong&gt; each client uploads &lt;strong&gt;one&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A voice room naturally splits participants into &lt;strong&gt;publishers&lt;/strong&gt; (people on mic seats) and &lt;strong&gt;subscribers&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;Practical tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus with DTX&lt;/strong&gt; (discontinuous transmission) cuts bandwidth when a speaker is silent, which is most of the time in a 12-seat room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio levels / active speaker&lt;/strong&gt; events from the SFU drive the "speaking ring" animation around seats; don't compute it on each client from raw audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens, not trust:&lt;/strong&gt; issue short-lived access tokens from your backend with explicit grants (&lt;code&gt;canPublish&lt;/code&gt;, &lt;code&gt;canSubscribe&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale by room, not by user:&lt;/strong&gt; pin a room to one SFU node where possible; for very large audiences, cascade or relay to edge nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Seats are state — keep them on the server
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RoomState {
  roomId, ownerId, mode, passwordHash?,
  seats: [ { index, userId|null, locked, mutedByHost } x 12 ],
  admins: [userId], version
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;All seat changes (&lt;code&gt;take&lt;/code&gt;, &lt;code&gt;leave&lt;/code&gt;, &lt;code&gt;lock&lt;/code&gt;, &lt;code&gt;mute&lt;/code&gt;, &lt;code&gt;kick&lt;/code&gt;) go through the backend as commands.&lt;/li&gt;
&lt;li&gt;The backend validates permissions, applies the change atomically (e.g. a Redis transaction or a row lock), bumps &lt;code&gt;version&lt;/code&gt;, and broadcasts the new state over your signaling/data channel.&lt;/li&gt;
&lt;li&gt;Clients render state; they never mutate it locally except for optimistic UI that gets reconciled.&lt;/li&gt;
&lt;li&gt;When a seat is lost, &lt;strong&gt;revoke publish permission on the SFU&lt;/strong&gt; as well — UI state and media permissions must agree, or a kicked user can keep talking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Coins and gifts: server-authoritative, always
&lt;/h2&gt;

&lt;p&gt;Gifting is where money lives, so the rule is absolute: &lt;strong&gt;the client never computes balances.&lt;/strong&gt; It sends intents; the server decides.&lt;/p&gt;

&lt;p&gt;A safe gift flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client sends &lt;code&gt;SendGift { giftId, qty, toUserId, roomId, idempotencyKey }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Server looks up the gift price from its own catalog (never trust a price from the client).&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;one database transaction&lt;/strong&gt;: 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.&lt;/li&gt;
&lt;li&gt;Commit, then publish a &lt;code&gt;GiftSent&lt;/code&gt; event for animations and leaderboard updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Design details that save you later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Double-entry ledger:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency keys:&lt;/strong&gt; mobile networks retry. Without an idempotency key, a flaky connection turns one gift into three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integer minor units:&lt;/strong&gt; store amounts as integers; no floats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboards from events:&lt;/strong&gt; hourly/daily/weekly rankings can be Redis sorted sets fed by &lt;code&gt;GiftSent&lt;/code&gt; events, rebuilt from the ledger if needed. Keep them eventually consistent — the ledger stays strongly consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment webhooks:&lt;/strong&gt; coin purchases should be credited only after a verified payment-provider webhook or receipt validation, not when the client says "payment succeeded."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits and anomaly checks:&lt;/strong&gt; cap gifts per second per user and flag unusual patterns (new account + large transfers) for review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Moderation as a first-class feature
&lt;/h2&gt;

&lt;p&gt;Voice is harder to moderate than text because it is ephemeral. Build layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role model:&lt;/strong&gt; owner → room admins → seated speakers → listeners. Encode it in the token grants and in the backend command checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host tools:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text chat filtering:&lt;/strong&gt; run messages through a filter service before broadcast; let listeners filter the chat view (all / messages / gifts) so gift spam doesn't bury conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting + audit log:&lt;/strong&gt; every moderation action and report is written with actor, target, room, and timestamp. That log is how support resolves disputes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-level controls:&lt;/strong&gt; global bans, device/account risk signals, and visible safety banners in rooms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy and legal fit:&lt;/strong&gt; if the room embeds mini-games or virtual currency, check app store rules and local regulations for each market before you ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. A reference layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile app ──(WebRTC audio)──&amp;gt; SFU (LiveKit/mediasoup)
     │                            ▲ permissions via server API
     └──(WebSocket commands)──&amp;gt; Room service ──&amp;gt; Redis (room state, leaderboards)
                                   │
                                   └──&amp;gt; Wallet service ──&amp;gt; SQL ledger (transactions)
                                               ▲
                            Payment webhooks ──┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;strong&gt;media, room state, and money&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;About the author: I work on real-time voice and live streaming products at Fah Swe, a software company in Sylhet, Bangladesh — &lt;a href="https://fahswe.com" rel="noopener noreferrer"&gt;fahswe.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>architecture</category>
      <category>programming</category>
      <category>flutter</category>
    </item>
  </channel>
</rss>
