🎙️ Realtime Comms — Proposed / Historical Roadmap

Lifecycle: PROPOSED / HISTORICAL SUPPORTING
This page preserves the original planning narrative (Fluxer comparison, Helm phases, sizing speculation).
Current pointer (what ships in repo): Realtime Comms (current)
Evidence of shipped pieces: frontend/src/lib/comms/voice.ts, nakama-modules/livekit_rpc.ts, gitops/platform/base/livekit/ (raw manifests; default host wss://funday.gg/livekit).
Do not treat Helm commands or unverified HA/Redis claims below as production truth.

🎙️ Funday Realtime Comms Architecture

Lifecycle: MIXED — PARTIAL SHIP + PROPOSED ROADMAP
Shipped in repo: LiveKit frontend client (frontend/src/lib/comms/voice.ts), Nakama LiveKit RPCs (nakama-modules/livekit_rpc.ts), and raw/Kustomize GitOps manifests under gitops/platform/base/livekit/ (Deployment/Service/Ingress — not the Helm install commands below).
Default token/host URL in RPC code: wss://funday.gg/livekit (verify ingress before treating alternate hosts like livekit.funday.gg as canonical).
Still proposed / do not treat as fact: Helm-centric install phases, Redis clustering/HA sizing claims, and unverified UI/video/screen coverage.
Keep Nakama chat/session shell as baseline — Nakama multiplayer · Bridge.

Chat + Voice + Video for hierarchical lobbies (match → tournament → community → channel) Last verified: 2026-04-30 · Sources: Fluxer GitHub, LiveKit docs, Nakama docs, Funday codebase


🎯 TL;DR Verdict

question: Should we rewire to Fluxer for chat+voice+video?
answer: NO — Fluxer is the WRONG tool for embedding into Funday.
why:
  - Fluxer is a Discord-CLONE app (full Electron client + backend)
  - NOT a library/SDK — it's a whole product
  - Heavy stack (Erlang+TS+Rust+Cassandra+Meilisearch+Valkey+LiveKit)
  - Replaces Nakama entirely — destroys our existing chat infrastructure
  - We already have ~691 chat references across 36 frontend files
recommendation: Nakama (keep) + LiveKit (add)
why_this:
  - Fluxer itself uses LiveKit for voice/video → use LiveKit directly
  - Nakama already gives us hierarchical channels (room/group/DM)
  - LiveKit = Go + Pion WebRTC (matches our stack), K3s-native, SFU
  - Bridge them via shared room/channel ID convention

🔍 What is Fluxer?

AspectReality
📦 TypeFull Discord-clone application (not embeddable lib)
🏗️ BackendTypeScript + Hono + Erlang/OTP gateway
🖥️ ClientReact + Electron (desktop)
🦀 Hot pathRust → WASM
💾 StorageSQLite default, Cassandra optional
🔍 SearchMeilisearch
CacheValkey (Redis-compatible)
🎤 Voice/VideoLiveKit (← the actual engine)
📜 LicenseOpen source, self-hostable
🌐 Projecthttps://fluxer.app · https://github.com/fluxerapp/fluxer

Key insight: Fluxer IS LiveKit + a Discord UI. We don’t want the UI — we have ours.


flowchart TD
    subgraph Client ["SvelteKit Frontend"]
        NS[Nakama Socket WS]
        LKC[LiveKit Client WS+WebRTC]
    end

    subgraph Cluster ["K3s Infrastructure"]
        subgraph Nak ["Nakama Services"]
            NK[Nakama Pod]
            NKDB[(PostgreSQL)]
        end
        subgraph LK ["LiveKit Services"]
            LKS[LiveKit SFU Pod]
            LKRD[(Redis Clustering)]
        end
    end

    NS -->|Text chat, presence, matches| NK
    LKC -->|Voice, video, screen| LKS
    NK <-->|RPC bridge, token issuance| LKS
    NK --> NKDB
    LKS --> LKRD

🪝 Hierarchy Mapping (Single ID strategy)

flowchart LR
    Pattern["[scope]:[type]:[id]"] -->|shared| Nakama["Nakama Channel Name"]
    Pattern -->|shared| LiveKit["LiveKit Room Name"]
Funday ConceptNakama ResourceLiveKit RoomID Pattern
🎮 Game MatchMatch-scoped channelRoom (auto-cleanup on match end)match:<gameId>:<matchId>
🏆 TournamentGroupRoom (persistent, lifetime of tourney)tourney:<tournamentId>
🌐 Community LobbyGroup + room channelsRoom (always-on)community:<communityId>
📡 Channel (multi-user)Persistent channelRoom (on-demand)channel:<communityId>:<channelId>
💬 DMDM channelRoom (1-on-1, ephemeral)dm:<userIdA>:<userIdB>

⚖️ Decision Matrix

SolutionTypeVoice/VideoSelf-HostEmbed in AppK8sEffortVerdict
🟢 Nakama + LiveKitLib/SFU✅ WebRTCMedPICK
🟡 Nakama + MediasoupLib/SFU✅ WebRTCHighLower-level
🟡 Nakama + JitsiLib/MCU✅ WebRTC⚠️ iframeLowUI baked-in
🟡 Nakama + Pion (custom)Lib/raw✅ WebRTCVery HighOverkill
🟡 Nakama + Daily.coHostedN/ALow$$$
🟡 Nakama + TwilioHostedN/ALow$$$$
🔴 Fluxer (full replace)App✅ (LiveKit inside)MaybeMassiveREJECT
🔴 MattermostApp⚠️ via pluginMassiveTeam-chat focused
🔴 Rocket.ChatApp✅ via Jitsi⚠️ iframeMassiveWrong UX
🔴 Matrix/SynapseFederated✅ via Jitsi/Element Call⚠️ ElementMassiveFederation overkill

🎤 LiveKit Deep Dive

PropertyValue
🛠️ LanguageGo (matches Funday backend)
🌐 ProtocolWebRTC SFU (Selective Forwarding Unit)
🧩 Built onPion WebRTC (Go)
📡 SignalingWebSocket
🔐 AuthJWT tokens (room+permission grants)
📊 ScalabilityHorizontal, peer-to-peer routing via Redis
🐳 K8sHelm chart available
📜 LicenseApache 2.0
💰 Cost (self-host)~$0 for <500 concurrent; ~30-50% cheaper than Cloud at scale

📐 Resource Sizing (rough)

Concurrent RoomsCPURAMNotes
10–502 cores4 GBSingle node fits in funday.gg
50–2004 cores8 GBSingle node still ok
200–1000Multi-node + Redis16+ GBAdd SFU replicas

Funday today: ~17 games, modest concurrency → single LiveKit pod easily covers needs.


🔌 Integration Pattern (Nakama ↔ LiveKit)

1. Token Issuance via Nakama RPC

// nakama-modules/livekit_rpc.ts
import { AccessToken } from "livekit-server-sdk"
 
export const rpcLiveKitToken: nkruntime.RpcFunction = (ctx, logger, nk, payload) => {
  const { scope, type, id } = JSON.parse(payload) // e.g. {scope:'match', type:'connect4', id:'abc123'}
  const roomName = `${scope}:${type}:${id}`
 
  // Authorization: verify user has access to this Nakama resource
  const userId = ctx.userId
  if (!userCanAccess(nk, userId, scope, type, id)) {
    throw Error("forbidden")
  }
 
  const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
    identity: userId,
    name: ctx.username,
    metadata: JSON.stringify({ scope, type, id }),
  })
  at.addGrant({
    room: roomName,
    roomJoin: true,
    canPublish: true,
    canSubscribe: true,
    canPublishData: false, // Nakama owns text data
  })
  return JSON.stringify({ token: at.toJwt(), url: "wss://livekit.funday.gg", room: roomName })
}

2. Frontend Bridge

// frontend/src/lib/comms/voice.ts
import { Room, RoomEvent, Track } from "livekit-client"
import { nakama } from "$lib/server/nakama"
 
export async function joinVoice(scope: string, type: string, id: string) {
  const session = await nakama.getSession()
  const { token, url, room } = await nakama.callRpc(session, "livekit_token", { scope, type, id })
 
  const lkRoom = new Room({ adaptiveStream: true, dynacast: true })
  await lkRoom.connect(url, token)
  await lkRoom.localParticipant.setMicrophoneEnabled(true)
  return lkRoom
}

3. Lifecycle Sync (via Nakama hooks)

// nakama-modules/match-lifecycle.ts
// On match end → close LiveKit room
async function onMatchEnded(matchId: string) {
  await fetch(`https://livekit.funday.gg/twirp/livekit.RoomService/DeleteRoom`, {
    method: "POST",
    headers: livekitAuthHeaders(),
    body: JSON.stringify({ room: `match:${gameId}:${matchId}` }),
  })
}

🚀 Deployment Plan (HISTORICAL/PROPOSED Helm path — current GitOps uses raw manifests)

Phase 1: LiveKit Server

# Helm install
helm repo add livekit https://helm.livekit.io
helm install livekit livekit/livekit-server \
  -n livekit --create-namespace \
  --set redis.enabled=true \
  --set livekit.keys.<API_KEY>=<API_SECRET>

Phase 2: Ingress (nginx → Traefik:32443 → livekit-svc)

# gitops/platform/base/livekit/livekit-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: livekit-ingress
  namespace: livekit
  annotations:
    traefik.ingress.kubernetes.io/router.priority: "950"
spec:
  tls:
    - hosts: [funday.gg]
      secretName: funday-tls-cert
  rules:
    - host: funday.gg
      http:
        paths:
          - path: /livekit
            pathType: Prefix
            backend: { service: { name: livekit, port: { number: 7880 } } }

Phase 3: TURN/UDP (firewall)

# UDP 50000-60000 for media
ufw allow 50000:60000/udp
# TCP 7881 for fallback
ufw allow 7881/tcp

Phase 4: Frontend Module

pnpm add livekit-client
# Add /lib/comms/voice.ts + /lib/comms/video.ts wrappers

Phase 5: Nakama RPC

pnpm add livekit-server-sdk
# Add nakama-modules/livekit_rpc.ts
# Register in index.ts: initializer.registerRpc('livekit_token', rpcLiveKitToken);

🧪 Roadmap (Practical Steps)

#StepEffortRisk
1Deploy LiveKit to K3s + ingressSLow
2Add livekit_token RPC in nakama-modulesSLow
3Add /lib/comms/voice.ts SvelteKit moduleSLow
4Wire Connect4/match drawer ”🎤 Join voice” buttonMMed
5Wire community/tournament voice roomsMMed
6Add video toggle (publish video track)SLow
7Add screen shareSLow
8Egress (recording) — optionalMLow
9Adaptive bitrate + simulcast tuningMLow

🎁 Bonus: What We Keep (NO Fluxer-rewire needed)

✅ Nakama channels (socialRuntime, socialChat, matchChat) ✅ ChatView, GameChat, Chat.svelte, ChatWindow, ChatBubbleOverlay ✅ Chat moderation (chat-moderation.ts) ✅ Cross-tab sync, presence, friend list ✅ Identity SSOT (funday-identity cookie) ✅ All 691 existing chat refs across 36 files

Net change: +1 LiveKit pod, +1 RPC, +2 frontend modules, ~0 lines of existing chat code modified.


💡 Genius Improvements

UID💎 Improvement🛠️ What Changes✨ Benefit
A1🎤 Push-to-talk in match drawerWire Space key → setMicrophoneEnabled(true)Gaming-grade voice without echo
A2📺 Spectator video stream from match hostMatch host publishes screen track; spectators subscribeTournament casting without OBS
A3🧠 Voice-aware presence in NakamaLiveKit webhook → Nakama presence flag voice:onShow 🎤 icon next to active speakers
A4🎭 Per-channel voice permissionsUse Nakama group roles → LiveKit canPublish grantMute “muted” users system-wide
A5🔇 Speaker-only voice roomsTournament finals: only finalists publish, viewers subscribeBroadcast-style esports rooms
A6📼 Auto-record tournament finalsLiveKit Egress → S3 (or local PVC)Replay/highlights pipeline
A7🎚️ Spatial audio in game lobbiesLiveKit spatial audio API + game positionProximity chat for open-world games
A8🤖 AI moderation on voiceLiveKit → Whisper → ChadG moderationUnified text+voice moderation
A9🎬 Video reactions in chatShort clip publish + post URL to Nakama channelTikTok-style emote video reactions
A10🌐 Cross-game community voice roomsPersistent LiveKit rooms keyed to community IDAlways-on community hangouts

🔗 Sources


🎯 Final Answer

fluxer: NO — wrong abstraction level (whole app, not embeddable)
recommendation: Nakama (keep all 691 refs) + LiveKit (add for voice/video)
hierarchy_bridge: shared <scope>:<type>:<id> naming convention
deployment: 1 K3s helm chart + 1 RPC + 2 frontend modules
effort: ~1 sprint to MVP, ~2 sprints with screen-share + recording
risk: low — both stacks are battle-tested in our environment

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send