🎙️ 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 hostwss://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 undergitops/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 likelivekit.funday.ggas 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?
| Aspect | Reality |
|---|---|
| 📦 Type | Full Discord-clone application (not embeddable lib) |
| 🏗️ Backend | TypeScript + Hono + Erlang/OTP gateway |
| 🖥️ Client | React + Electron (desktop) |
| 🦀 Hot path | Rust → WASM |
| 💾 Storage | SQLite default, Cassandra optional |
| 🔍 Search | Meilisearch |
| ⚡ Cache | Valkey (Redis-compatible) |
| 🎤 Voice/Video | LiveKit (← the actual engine) |
| 📜 License | Open source, self-hostable |
| 🌐 Project | https://fluxer.app · https://github.com/fluxerapp/fluxer |
Key insight: Fluxer IS LiveKit + a Discord UI. We don’t want the UI — we have ours.
🏛️ Recommended Architecture: Nakama + LiveKit
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 Concept | Nakama Resource | LiveKit Room | ID Pattern |
|---|---|---|---|
| 🎮 Game Match | Match-scoped channel | Room (auto-cleanup on match end) | match:<gameId>:<matchId> |
| 🏆 Tournament | Group | Room (persistent, lifetime of tourney) | tourney:<tournamentId> |
| 🌐 Community Lobby | Group + room channels | Room (always-on) | community:<communityId> |
| 📡 Channel (multi-user) | Persistent channel | Room (on-demand) | channel:<communityId>:<channelId> |
| 💬 DM | DM channel | Room (1-on-1, ephemeral) | dm:<userIdA>:<userIdB> |
⚖️ Decision Matrix
| Solution | Type | Voice/Video | Self-Host | Embed in App | K8s | Effort | Verdict |
|---|---|---|---|---|---|---|---|
| 🟢 Nakama + LiveKit | Lib/SFU | ✅ WebRTC | ✅ | ✅ | ✅ | Med | PICK |
| 🟡 Nakama + Mediasoup | Lib/SFU | ✅ WebRTC | ✅ | ✅ | ✅ | High | Lower-level |
| 🟡 Nakama + Jitsi | Lib/MCU | ✅ WebRTC | ✅ | ⚠️ iframe | ✅ | Low | UI baked-in |
| 🟡 Nakama + Pion (custom) | Lib/raw | ✅ WebRTC | ✅ | ✅ | ✅ | Very High | Overkill |
| 🟡 Nakama + Daily.co | Hosted | ✅ | ❌ | ✅ | N/A | Low | $$$ |
| 🟡 Nakama + Twilio | Hosted | ✅ | ❌ | ✅ | N/A | Low | $$$$ |
| 🔴 Fluxer (full replace) | App | ✅ (LiveKit inside) | ✅ | ❌ | Maybe | Massive | REJECT |
| 🔴 Mattermost | App | ⚠️ via plugin | ✅ | ❌ | ✅ | Massive | Team-chat focused |
| 🔴 Rocket.Chat | App | ✅ via Jitsi | ✅ | ⚠️ iframe | ✅ | Massive | Wrong UX |
| 🔴 Matrix/Synapse | Federated | ✅ via Jitsi/Element Call | ✅ | ⚠️ Element | ✅ | Massive | Federation overkill |
🎤 LiveKit Deep Dive
| Property | Value |
|---|---|
| 🛠️ Language | Go (matches Funday backend) |
| 🌐 Protocol | WebRTC SFU (Selective Forwarding Unit) |
| 🧩 Built on | Pion WebRTC (Go) |
| 📡 Signaling | WebSocket |
| 🔐 Auth | JWT tokens (room+permission grants) |
| 📊 Scalability | Horizontal, peer-to-peer routing via Redis |
| 🐳 K8s | Helm chart available |
| 📜 License | Apache 2.0 |
| 💰 Cost (self-host) | ~$0 for <500 concurrent; ~30-50% cheaper than Cloud at scale |
📐 Resource Sizing (rough)
| Concurrent Rooms | CPU | RAM | Notes |
|---|---|---|---|
| 10–50 | 2 cores | 4 GB | Single node fits in funday.gg |
| 50–200 | 4 cores | 8 GB | Single node still ok |
| 200–1000 | Multi-node + Redis | 16+ GB | Add 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/tcpPhase 4: Frontend Module
pnpm add livekit-client
# Add /lib/comms/voice.ts + /lib/comms/video.ts wrappersPhase 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)
| # | Step | Effort | Risk |
|---|---|---|---|
| 1 | Deploy LiveKit to K3s + ingress | S | Low |
| 2 | Add livekit_token RPC in nakama-modules | S | Low |
| 3 | Add /lib/comms/voice.ts SvelteKit module | S | Low |
| 4 | Wire Connect4/match drawer ”🎤 Join voice” button | M | Med |
| 5 | Wire community/tournament voice rooms | M | Med |
| 6 | Add video toggle (publish video track) | S | Low |
| 7 | Add screen share | S | Low |
| 8 | Egress (recording) — optional | M | Low |
| 9 | Adaptive bitrate + simulcast tuning | M | Low |
🎁 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 drawer | Wire Space key → setMicrophoneEnabled(true) | Gaming-grade voice without echo |
| A2 | 📺 Spectator video stream from match host | Match host publishes screen track; spectators subscribe | Tournament casting without OBS |
| A3 | 🧠 Voice-aware presence in Nakama | LiveKit webhook → Nakama presence flag voice:on | Show 🎤 icon next to active speakers |
| A4 | 🎭 Per-channel voice permissions | Use Nakama group roles → LiveKit canPublish grant | Mute “muted” users system-wide |
| A5 | 🔇 Speaker-only voice rooms | Tournament finals: only finalists publish, viewers subscribe | Broadcast-style esports rooms |
| A6 | 📼 Auto-record tournament finals | LiveKit Egress → S3 (or local PVC) | Replay/highlights pipeline |
| A7 | 🎚️ Spatial audio in game lobbies | LiveKit spatial audio API + game position | Proximity chat for open-world games |
| A8 | 🤖 AI moderation on voice | LiveKit → Whisper → ChadG moderation | Unified text+voice moderation |
| A9 | 🎬 Video reactions in chat | Short clip publish + post URL to Nakama channel | TikTok-style emote video reactions |
| A10 | 🌐 Cross-game community voice rooms | Persistent LiveKit rooms keyed to community ID | Always-on community hangouts |
🔗 Sources
- 📘 https://docs.livekit.io/transport/self-hosting/
- 📘 https://docs.livekit.io/transport/data/state/room-metadata/
- 📘 https://docs.livekit.io/frontends/authentication/tokens/
- 📘 https://heroiclabs.com/docs/nakama/concepts/chat/
- 📘 https://heroiclabs.com/docs/nakama/server-framework/streams/
- 🐙 https://github.com/livekit/livekit
- 🐙 https://github.com/fluxerapp/fluxer (← reference only, NOT for embedding)
🎯 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