Social LiveDocumentationPLATFORM DOCS

Live Rooms

Room lifecycle, roles, stage guests, audio party rooms, backgrounds, and PK battles.

Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

TL;DR -A live room is the heart of Social Live: one host broadcasts, viewers join and chat, and paid gifts fly across the screen. This page covers everything that happens inside a room.

Who this is for -Product & Design: the plain-terms and feature sections up top. Business: the gifting behaviors. Engineering: everything, including endpoints, data model, and invariants.

A live room is a live broadcast anyone can join. One person -the host -is on stage with their camera and mic. Everyone else is a viewer who watches, types in the chat, and sends gifts (animated icons bought with coins) to cheer the host on. The host can also invite viewers up onto the stage as guests.

Think of it like a tiny live TV show that any fan can jump into, comment on, and tip. The rest of this page walks through how a room is born, who can do what inside it, how gifts and chat behave, and the controls a host has to keep things running.

What a room is, in plain terms

  • A room has one host and many viewers. The host controls the room: they can end it, invite guests on stage, and remove troublemakers.
  • Rooms come in two flavors: video (live) and voice-only (voice, called an audio party room).
  • The stage is the set of people currently broadcasting -the host, plus any guests they've approved. Viewers can raise their hand to join the stage.
  • If a host's connection drops, the room doesn't die instantly. There's a short grace window to reconnect, so a flaky signal doesn't kill a live show.
  • Gifts are the money moment. Viewers tap a gift, it's charged to their coins, and it animates for everyone. Hosts (and on-stage guests) earn from gifts sent to them.

Everything below adds the technical detail beneath each of these ideas.

Room lifecycle

A room moves through a handful of states from the moment a host opens the create screen to the moment it ends. This diagram shows those states and the events that move between them.

Rendering diagram…
  • Creation -host picks title (with optional styled title: font/align/ effect stored as sanitized JSON titleStyle), room kind (live video or voice -see Audio Party Rooms), privacy, and a visual background (LiveRoomBackground). The host becomes a LiveParticipant with role HOST.
  • Grace window -if a host drops (app crash, network), the room enters a host-closed state with a 5-minute timer. Rejoining within the window resumes seamlessly; otherwise the room disappears from discovery and is ended. This prevents flapping rooms from a flaky connection.
  • Counts -viewer/participant counters are incremented on join and decremented with floors (never negative), reconciled against actual gateway audience.

Roles

There are three roles in a room, and they differ by what media they can send and what powers they have. Viewers only receive; hosts and guests can broadcast.

RoleMediaPowers
HOSTPublish audio+videoEnd room, approve/remove guests, kick/block anyone, start PK battles
GUESTPublish (audio-only or A+V, chosen at accept time)On-stage participant
VIEWERReceive onlyChat, gift, request stage

Stage flow: viewer requests → host approves → guest picks media mode → guest's producers appear for everyone (see Media & SFU).

Gifts inside a room

Gifts are the core paid interaction. A viewer taps a gift, it's charged to their coin balance, and it animates on everyone's screen. Here's how that works under the hood.

Gifts are charged over REST (POST /wallet/gifts/send, idempotent -see Gifting) and broadcast over the live gateway. Recipients default to the host but can target any stage participant. Self-gifting is always blocked, which also prevents hosts pumping their own rooms.

The gift tray supports multi-recipient sends: a horizontal avatar row of everyone on stage (host first) sits above the gift grid with an "All" select-all button. Tapping a gift sends it to every selected person -the client issues one POST /wallet/gifts/send (fresh idempotency key each) and one gift:send socket announce per recipient, so each send is its own ledger transaction and a partial failure (e.g. running out of coins midway) never corrupts the ones already settled.

Combos: after a send, a floating combo button holds a 3-second window — each tap resends the same gift to the same recipients with an incrementing comboCount (display-only; every tap is its own idempotent wallet charge, and a rejected charge stops the run instead of announcing an unpaid gift). The server rewrites the sender's gift line in scrollback ("Sent Rose ×27") rather than inserting one row per tap, and clients update that feed line in place.

Every stage tile (host included) shows a session gift counter: coins gifted to that person during this live. The gateway accumulates per-room totals (coin value read from the catalog, never the client) and includes recipientId / coinCost / recipientTotal on gift:sent events; the counter is display-only session state -diamonds are settled by the wallet at gift time -and resets when the live ends. Joining a different room always starts from zero. Counters render short-scale abbreviated once they reach the thousands (5,004 → 5k, 5,243 → 5.2k, 1,300,000 → 1.3M; truncated, never rounded up).

Send animations: every gift:sent also queues an animation in the room overlay (above the stage, tap-through, one at a time -floods keep only the freshest few). Gifts with a remote Lottie animation URL play it; everything else gets an icon/emoji burst, so gifts animate even before real assets ship. Premium sends (≥100 coins or combo ×10+) render bigger and longer -the tier-escalation hook for the SL-22 asset catalog.

Top gifters: the gateway also accumulates per-sender session totals; gift:sent events carry a topGifters top-3 snapshot and the stage-participants rejoin payload includes it, so the room header shows a compact overlapping avatar stack of the top two gifters (gold/silver rank rings) beside a bean total pill (sum of every ranked gifter's coins). Tapping the stack or the pill opens the gifter leaderboard flyout -every gifter ranked highest-first with their bean amount on the right; tapping a row opens that gifter's mini profile. Session-scoped, resets when the live ends.

Each stage box also shows a bean counter (coins gifted to that member this session). Tapping it opens a per-guest flyout -who gifted that member, sorted highest first (GET /live/rooms/:id/gifters/:recipientId, authenticated, session-scoped like the leaderboard).

Room chat

The chat feed is the running conversation beside the stage. It survives leaving and rejoining because messages are stored per room, not just streamed live.

Chat messages (and gift lines) are persisted per room in LiveRoomMessage -sender name/avatar/level snapshotted at send time, rows cascade-deleted with the room. GET /live/rooms/:id/messages returns up to the last 500, oldest first; clients seed their feed from it on every (re)join, merged around whatever realtime events already arrived, so leaving and coming back never loses the scrollback. The feed is fully scrollable back to the first message of the stream.

Stage tiles show the participant's profile photo whenever their video isn't rendering -camera off, audio seats, still connecting, or host paused.

Comment actions: long-pressing any chat comment opens a sheet with Reply and Copy (plus Pin for the host and Report for others). Reply attaches a quote to the next comment: the chat:message socket payload carries replyToId, the server resolves the target from the stored row (authoritative -a client can't spoof the quoted sender/text) and persists a snapshot (replyToId / replyToSenderName / replyToPreview) on the LiveRoomMessage, so the quote survives leave/rejoin and renders above the comment for everyone. The composer shows a cancelable quote bar while replying.

Pinned announcement: the host can pin one message (Room controls → Pin announcement, max 200 chars) that renders as a 📌 banner above the chat feed for everyone, including late joiners -it lives on the room (LiveRoom.pinnedMessage, POST /live/rooms/:id/pin, empty body unpins) and fans out over the standard room-updated event. The pin dies with the room. The host can also long-press any chat comment to pin it -the request sends messageId instead of text and the server snapshots the sender from the stored chat row, so the banner shows the comment exactly as typed, with the sender's avatar and name.

Entrance banners

When a notable user walks in, the room shows a little animated "so-and-so joined" banner. Only higher-level users trigger it, and the level is server-verified so nobody can fake a fancy entrance.

Joins by users above the chat-badge level threshold (>5) trigger a short animated entrance banner above the chat feed -avatar, level badge, and "{name} joined", tinted by the level color ramp. The gateway enriches live:viewer_joined with a server-resolved level and avatar (a client can never claim a tier), and the client queues banners one at a time (~2.4s, max 3 pending) so join floods never drown the room. VIP tiers slot into the same banner when VIP subscriptions ship.

Profile flyouts

Tapping anyone in the room opens a mini profile card with quick actions -follow, message, or @-mention them into the chat.

Tapping anyone in the room -the host avatar (top-left), a guest tile on stage, a commenter's avatar in the chat feed, or a row in the viewers sheet -opens a mini profile flyout: photo, name, username, level, bio, follower stats, plus Follow, Message (chains into the in-room DM thread) and an @ mention action that pre-fills the room chat composer with @username and opens the keyboard. Tapping the flyout's avatar jumps to the full public profile, where tapping the photo opens it fullscreen (pinch-zoom viewer).

Host moderation: kick & block

The host is the room's referee. From anyone's profile card they can remove or ban a disruptive person.

When the host opens anyone's flyout, a 3-dots menu (top-right) offers Block, Kick, and Kick & Block:

  • Kick emits room:kick on the live gateway. The server verifies the sender is the room host (the host can never be a target), announces moderation:room_action { action: "kick", userId } to the room, detaches the target's sockets, closes their media producers, removes any stage presence, and bars them from rejoining (room:join is rejected) for as long as the room stays live. The kicked client tears down media and exits to home with a "removed by the host" notice.
  • Block reuses the standard block API (POST /users/:id/block) -see Moderation.
  • Kick & Block performs both.

The kick ban list is in-gateway-memory per room and clears when the room ends.

Viewer list visibility

The host decides whether everyone can see who is watching, or just the host. The viewer count always stays public -only the list of names can be hidden.

The host chooses whether the room's viewer list is visible to everyone or to the host alone. It is set at creation (create screen → Viewer list: Everyone / Host only, default Everyone) and toggled live from Room controls → Privacy. The flag rides LiveRoom.viewerListPublic and fans out on the standard room-updated event, so every client flips in sync.

Only the list of who is watching is gated -the viewer count (the eyes pill) stays visible to everyone. GET /live/rooms/:id/viewers is authenticated: when a room is host-only and the requester is not the host, it returns the count with an empty roster and hidden: true, so a crafted client can never read the hidden list. The host toggles it with POST /live/rooms/:id/viewer-visibility ({ isPublic }, host-only). Tapping the eyes as a non-host on a hidden room shows a "host has hidden the viewer list" notice instead of the sheet.

Comment lock

The host can freeze the chat entirely -useful to calm a room down. While locked, nobody (not even the host) sees the feed, and the backend refuses new comments too.

The host can lock room comments (Room controls → Privacy → Lock comments). While locked the chat feed is hidden for everyone (host included), the comment box renders disabled ("Comments are locked"), and an open typing overlay is closed on the spot. The backend enforces it too: createComment rejects with 403 while LiveRoom.commentsLocked is true, covering both the REST route and the socket send-comment path. The host toggles it with POST /live/rooms/:id/comments-locked ({ locked }, host-only); the flag rides the standard room-updated broadcast so all clients flip in sync. Unlocking restores the feed including scrollback — nothing is deleted.

DMs from inside a room

Viewers can read and answer their direct messages without leaving the live -the inbox slides open on top of the room.

The composer has a Messages button (unread badge included) that opens the DM inbox as a flyout over the room -viewers read and reply to DMs without leaving the live. It reuses the chat module's conversation list and thread screens in a nested navigator; unread counts refresh from the chat socket's realtime message stream.

PK battles

Two hosts go head-to-head and the audience decides the winner by gifting -whoever's gifts are worth more leads the score.

Two hosts battle head-to-head; gift value drives the score. State lives in PkBattle with status transitions broadcast to both rooms. Only the host of an existing live room may start one.

Discovery

Discovery is how viewers find rooms to join -a grid of what's live now, plus a feed of just the people you follow.

/discovery and /search list active rooms as API payloads (title, host, avatar, counts, background). A follow-scoped feed shows rooms only from accounts the viewer follows -and skips the query entirely when the user follows no one.

Available stage grids

A video room can show different numbers of on-stage seats. Which grid sizes are offered is set by admins, and the server always has the final say so a hacked client can't force a bigger grid.

The video grid sizes a host may pick are admin-configurable (Settings → Live rooms → Available stage grids, allowed_guest_slot_counts, stored as guest-slot counts: 0 solo, 1→2-grid, 2→3-grid, 3→6-grid… where an N-grid is host + guests). The default enables the small grids and disables the big 7/10 grids. GET /live/layout-options serves the allowed set; the create screen and in-room selector hide disabled grids (a room already on a now-disabled grid keeps its option until the host changes it). The server is authoritative -createRoom/updateRoomLayout snap any requested size down onto the allowed set after the room-kind clamp, so a crafted client request can't force a disabled grid. Voice rooms are exempt (they use audio seat layouts, not video grids).

Audio room layout

Voice rooms can render their seats in one of two styles, chosen by an admin.

Audio-party rooms render with one of two on-stage layouts, chosen by the admin (Settings → Live rooms → Audio room layout, audio_room_layout, served alongside the grids on GET /live/layout-options):

  • Seat grid (grid, default) -the classic seat tiles.
  • Bubbles (bubbles) -each member is a translucent, glowing "soapy" bubble whose diameter scales with how much they've been gifted this session (their gifter tier), so the biggest gifters read largest. Speaking members glow brighter.

The layout only affects audio-party rooms; video rooms are unaffected.

Host inactivity auto-shutdown

If a host silently vanishes (phone dies, no reconnect), the room can't run forever with nobody driving it. A configurable countdown ends it cleanly.

A host who fully leaves a room (app killed or connection lost, with no socket reconnect) would otherwise leave a ghost live running forever. When the host's last socket goes, the gateway starts an inactivity countdown:

  • The grace, in seconds, is admin-configurable (Settings → Live rooms → Host inactivity, host_inactivity_shutdown_seconds, default 300, clamped 30–3600). A bad value can never make rooms end instantly or hang.
  • The room's endedAt is stamped to now + grace, which hides it from discovery immediately and lets the lazy sweep reap it even across a server restart. Viewers already inside stay connected (join/getActiveRoom gate on isLive, not endedAt); counters are left intact.
  • A host reconnect within the window cancels the shutdown and un-hides the room (clearScheduledClose).
  • If the grace elapses, the room auto-ends via the normal end path, which broadcasts live:room_ended to the channel and evicts viewers.

This is distinct from an explicit host-close (a 5-minute grace with counters zeroed) -inactivity shutdown handles the host silently vanishing.

Sharing a live

Any room can be shared out to friends via a normal link. The link opens a small web page with a rich preview, and its button deep-links straight into the app.

The room's "…" menu has Share this live, which opens the OS share sheet (WhatsApp, SMS, contacts, …) with a link to a public invite landing: GET /live/:id/invite (unauthenticated, served from the API origin). The landing is a small HTML page whose OpenGraph tags drive the rich preview that messaging apps render -host name as the title, room title as the description, host avatar as the image when it is an absolute URL. For a missing, ended, or private room the landing renders a generic "join Social Live" page and leaks no room detail. Its CTA uses the sociallive:///live/:id deep link (triple slash -an empty host keeps the whole path visible to the app's URI parser). The sociallive scheme is registered natively on both platforms (iOS CFBundleURLTypes + FlutterDeepLinkingEnabled, Android VIEW/BROWSABLE intent-filter + flutter_deeplinking_enabled), and a GoRouter redirect normalizes the legacy host-form sociallive://live/:id to /live/:id, so tapping the button opens the room in the app.

Diagnostics

For debugging, room and media events can be logged and browsed in the admin portal behind a flag.

With ENABLE_LIVE_DIAGNOSTICS=true, room lifecycle and media events are logged to LiveRoomEventLog (payloads sanitized) and browsable in the admin Diagnostics page. LIVE_PERF_LOGS=true enables client-side performance logs in the Flutter app for live flows.