refactor/typescript-bun-vite
Backend (src/server, runs on Bun): - GibbzHttpServer (health, /api/ice, /api/rooms, static, keepalive tuning) - SignalingServer with Peer/Room/RoomRegistry classes and typed messages - IceConfigProvider (env STUN/TURN + coturn HMAC ephemeral creds) - StaticFileServer (dist/ with public/ fallback, traversal guard) - Tracing (OpenTelemetry SDK), AppConfig; createApp() factory for tests Frontend (src/client, built by Vite): - VoiceApp orchestrator implementing VoiceAppContext - WebRtcManager + PeerSession (perfect negotiation, pc rebuild, quality monitor) - SignalingClient + ReconnectController, AudioManager, SpatialAudio, Microphone/ScreenShare controllers, ScreensUi/RosterUi/SpatialMap/ SpeakingMonitor, Settings/Welcome dialogs, LayoutManager, MatrixChat Tooling: bun scripts (dev/start/test/typecheck), vite.config.ts root src/client with publicDir public/static, vitest-config for CI-less runs, bun-based Dockerfile (oven/bun:1), Makefile + README updated, legacy server.js/tracing.js/test.js/public js removed. Verified locally: tsc --noEmit clean, vitest run 6/6 green, vite build ok, server smoke-tested serving dist + signaling endpoints under Node. Local bun binary segfaults (SIGILL) on this host, so 'bun test' itself was not executable here; tests are vitest-API compatible which bun test maps.
gibbz voice — POC
Discord-clone proof of concept: peer-to-peer WebRTC voice chat + screen sharing with position-based spatial audio and pluggable STUN/TURN.
TypeScript everywhere: the backend runs on Bun, the client is built with Vite, and both are organized into classes.
Layout
src/server/ Bun (Node-compatible) WebSocket/HTTP backend
index.ts entrypoint + createApp() factory (used by tests)
http-server.ts GibbzHttpServer — health probes, /api/ice, /api/rooms, static
signaling.ts Peer / Room / RoomRegistry / SignalingServer (ws)
ice.ts IceConfigProvider — env-driven STUN/TURN + HMAC TURN creds
static-files.ts StaticFileServer — dist/ (or public/) with traversal guard
tracing.ts Tracing — OpenTelemetry SDK setup
config.ts AppConfig — env + static dir resolution
src/client/ Vite root (index.html, style.css)
app.ts VoiceApp — orchestrator implementing VoiceAppContext
context.ts the subsystem-facing application interface
state.ts AppState — shared cross-cutting voice state
webrtc/ WebRtcManager, PeerSession, SignalingClient, ReconnectController
media/ AudioManager (mic chain), SpatialAudio, Microphone-, ScreenShareController
ui/ roster/screens/spatial-map renderers, settings, welcome, layout
chat/ MatrixChat (OAuth2/OIDC text channel)
public/static/ PWA assets served verbatim (icons, manifest, service worker)
tests/ bun test / vitest compatible suite (tests/server.test.ts)
Develop
bun install
bun run dev:server # backend on :8080 (bun --watch)
bun run dev # vite dev server for the client
bun test # tests (vitest-compatible; `bun run test:node` = vitest run)
bun run typecheck # tsc --noEmit
bun run build # production client bundle into dist/
bun run start # serve dist/ + signaling (bun src/server/index.ts)
The server also runs under Node 22.18+/23+ (node src/server/index.ts) since it
only uses erasable TypeScript, but Bun is the supported runtime (the Dockerfile
is oven/bun:1). A bun.lock is generated on first bun install.
What works
- Mesh WebRTC voice (perfect negotiation; lowest peer id initiates), full pc-rebuild recovery on ICE failure with TURN-relay fallback
- Screen share with audio (mic is spatialized, screen audio plays direct), spotlight/grid tiles and pop-out windows
- Spatial audio: drag your avatar on the plane; remote mics run through a
PannerNode(HRTF, inverse distance). Positions sync over signaling. - Hi-Fi rooms (Code, Gaming): stereo Opus 256 kbps, echo cancellation off
- Custom STUN/TURN: gear icon → JSON override in
localStorage, applied on next join. Server default comes from env (below). - Mute (M), deafen (D), per-peer volume, speaking indicators, quality diagnostics (loss/jitter/RTT), auto-reconnect with countdown banner
- Matrix-backed text channel (OAuth2/OIDC against matrix.k6n.net)
ICE configuration (server side)
| Env var | Meaning |
|---|---|
STUN_URLS |
comma-separated stun: urls |
TURN_URLS |
comma-separated turn:/turns: urls |
TURN_USERNAME / TURN_CREDENTIAL |
static TURN creds |
TURN_SECRET |
coturn static-auth-secret → ephemeral HMAC creds |
ICE_TRANSPORT_POLICY |
WebRTC ICE policy: all, relay, or none |
ICE_SERVERS_JSON |
full {"iceServers":[...]} override, wins over the above |
PORT / HOST |
listen address (default 8080 / 0.0.0.0) |
POC shortcuts (productionize later)
- No auth, rooms are in-memory, signaling is plain ws behind the ingress.
- No media server: N² mesh — fine for small rooms, needs SFU beyond ~6–8 participants.
- Text channels are a single live Matrix room; no history pagination.
Languages
JavaScript
75.1%
CSS
16.2%
HTML
7.5%
Makefile
0.9%
Dockerfile
0.3%