Hall is the real-time layer under every Soulcraft product — video, voice, presence, chat, recording, broadcast, and live understanding. One calm page, every capability, small examples.
hall v0.6.0 · wire format: camelCase msgpackYour server keeps one long-lived WebSocket to Hall and authenticates with its product secret. Everything in @soulcraft/sdk — nothing new to install.
// server-side, once at startup
import { createHallModule } from '@soulcraft/sdk/server'
const hall = createHallModule({
url: 'wss://hall.soulcraft.com/ws/product',
productName: 'venue',
secret: process.env.HALL_VENUE_SECRET,
})
await hall.connect()
Products are added and removed in [auth.products] on the server and apply with a config reload — no Hall restart, no redeploy.
A room is one live session — a class, a stage, a huddle. Your server creates it, names it after your own entity id, and closes it when the moment ends.
const room = await hall.createRoom('cohort-42', {
maxPeers: 12,
enableTranscription: true,
enableRecording: false,
})
// ... later
await hall.closeRoom('cohort-42')
Room events stream back on the same connection: peerJoined, peerLeft, speakerChanged, transcript, conceptMention, recordingManifest.
Your server mints a short-lived signed token for each person; the browser joins with it. The token carries the role — participants publish, viewers only receive — and cannot be tampered with.
// server: mint a token (role: 'participant' | 'viewer')
const { token } = await hall.createSessionToken('cohort-42', userId, 300, 'participant')
// browser: join with it
import { joinHallRoom } from '@soulcraft/sdk/client'
const session = await joinHallRoom({ token, hallUrl: 'wss://hall.soulcraft.com' })
session.on('trackAdded', ({ peerId, track }) => attach(track))
Hall assumes networks blip and servers deploy — and makes both boring. When Hall must refuse or drop a connection it says exactly when to come back; when a connection drops abnormally, the peer's place in the room survives a grace window so reconnecting resumes instead of rejoining.
// the shed frame — sent before a 1013 close
{ t: 'shed', d: { code: 'SERVER_DRAINING', message: '...', retryAfterMs: 4000 } }
// reconnect inside the grace window (default 15s), same token:
{ t: 'sessionOk', d: { roomId, peerId, role, canPublish, resumed: true } }
Retry-After).peerLeft churn on a blip — presence stays truthful.Topic-based messaging with live presence and replay for late joiners — the "this room is live" chip, cursors on a doc, lightweight fan-out between your own services.
// server or browser (browser uses a pubsub token)
await hall.subscribeTopic('community:general', { name: 'Ada' })
await hall.broadcastTopic('community:general', { kind: 'wave' })
const here = await hall.getPresence('community:general')
Chat rides the room's data channel, fans out to everyone, and is retained in a capped ring — late joiners ask for the scroll-back.
// browser: send on the 'chat' channel
session.sendData('chat', { text: 'hello, room' })
// server: fetch history (last 50)
const { messages } = await hall.getChatHistory('cohort-42', 50)
// → [{ peerId, text, timestampMs }, ...]
Hall transcribes speech in-process (Whisper) and matches it against the concepts you care about (BERT embeddings) — no audio ever leaves the box. Your server receives understanding as it happens.
hall.on('transcript', ({ roomId, peerId, text, isFinal }) => ...)
hall.on('conceptMention', ({ nodeId, verbType, confidence, text }) => ...)
hall.on('relationProposed', ({ fromNodeId, verbType, toNodeId }) => ...)
Pass concepts at createRoom time (your graph nodes); use selectiveTranscriptionThreshold to transcribe only the active speaker in big rooms.
Per-participant audio and video tracks, written on Hall's data volume. Stopping returns a manifest of file paths — your product decides where they live forever.
await hall.startRecording('cohort-42')
// ... the session happens ...
const manifest = await hall.stopRecording('cohort-42')
// → { sessionId, audioTracks: [paths], videoTracks: [paths] }
Upload, store, transcode, and stream media with range requests and auto-thumbnails — with optional TTL so transient media cleans itself up.
POST /media/upload # multipart → { mediaId }
GET /media/{id} # stream, supports Range
GET /media/{id}/thumbnail # auto-generated webp
GET /media/{id}/info # metadata json
DELETE /media/{id}
External publishers (OBS, FFmpeg) push into a room over standard WHIP; big audiences watch over WHEP (sub-second) with automatic overflow to low-latency HLS.
# OBS → Settings → Stream → WHIP
Server: https://hall.soulcraft.com/whip/stage-night
Token: {product secret}
# viewers beyond the WebRTC cap fall back automatically
GET /hls/stage-night/playlist.m3u8
Recordings become VOD through a pull-based job queue. Workers — CPU on the box today, the GPU box's NVENC encoders when volume justifies — pull work when they have budget, so Hall can never overload them.
# a worker leases the next job it can encode
POST /worker/lease Authorization: Bearer {workerSecret}
{ workerId: 'gex44', capabilities: { nvenc: ['h264','hevc','av1'] } }
# → 200 { jobId, sourcePath, outputPath, codec } | 204 no work
POST /worker/complete { workerId, jobId }
POST /worker/fail { workerId, jobId, reason } # requeues, then fails loudly
GET /health — JSON status, version, rooms, products, counters.GET /stats — the live dashboard humans watch.GET /metrics — Prometheus, including queue depth and shed counters./admin/* — rooms, peers, token revocation, audit trail, config (Bearer auth).SIGHUP or POST /admin/config/reload applies [auth], [limits], [rate_limit] live; restart-bound sections warn loudly instead of half-applying.