Burble is a self-hostable voice communications platform built for people who care about latency, privacy, and control. Think Mumble’s audio seriousness meets modern browser-based joining — no downloads, no accounts, no friction.
One command to deploy. Sub-second to join. Your server, your rules.
Burble is pre-production (self-assessed CRG grade C, targeting B). The core — self-hosted WebRTC voice, browser join, OTP fault isolation — is operational and tested. Per ADR-0007, every claim below maps to code + a test, or is flagged here:
-
Experimental (optional, off by default): QUIC transport and SNIF WASM crash-isolation require optional NIFs (
quicer/msquic,wasmex) that are disabled in the default build; the runtime degrades gracefully without them. -
Hardware-gated: sub-microsecond PTP needs a PTP-capable NIC; without one the system uses NTP (~1ms). PTP hardware is unvalidated (I210 pending).
-
Roadmap: the Idris2 ABI proofs compile and type-check; runtime enforcement is not yet wired (ADR-0008, Option C; PoC tracked in issue #55).
-
Not yet benchmarked: end-to-end mic-to-speaker latency and 500+ concurrent scale (issue #52).
-
Signalling wire format: SDP bodies ride the Bebop binary plane by default since 2026-08-04 (PR #180), on codecs generated from
server/priv/schemas/voice_signal.bop. JSON remains an explicit opt-out (config :burble, :signaling_wire_format, :json) and the automatic fallback if a Bebop encode fails; decode of both planes is always accepted, so mixed-version peers keep working, and clients still receive a plainsdpstring. The plane satisfies spline ADR-0005 criterion (a); a second consumer parses the same bytes (criterion (b), gossamer#145) and the alignment is CI-enforced in groove (criterion (c), groove#30). -
Test gate: the full server suite passes and the CI pass/fail gate is armed — a failing test turns CI red (
elixir-ci.yml"Run server tests", re-armed 2026-07-07). The green gated run onmain(Elixir CI @2b5914b) is the receipt for the current self-assessed CRG grade C (issue #100 closed; see.machine_readable/descriptiles/STATE.a2ml). The Dialyzer lane remains advisory.
Foundational hardening is tracked in the "Earn the Core" epic (issue #53).
Known debt is enumerated, with the evidence for each item, in
DEBT.md — measured 2026-08-07 across build, supply chain,
CI/CD, licence, documentation, code, proof and test. It indexes rather than
replaces PROOF-NEEDS.md,
TEST-NEEDS.md and READINESS.adoc.
Read it before trusting a green badge or a passing gate here: several of the
findings are gates that cannot fail.
| Burble | Mumble | Discord | Jitsi | |
|---|---|---|---|---|
Self-host |
Yes |
Yes |
No |
Yes |
Browser join |
Yes (WebRTC) |
No (native only) |
Browser + app |
Yes |
Latency |
<10ms* (kernel-path target) |
~15ms |
~50-100ms |
~30ms |
Privacy |
DTLS-SRTP today; peer-held E2EE part-way † |
Encrypted, no telemetry |
Telemetry, scanning |
E2EE optional (vendor claim) |
Precision timing |
IEEE 1588 PTP (<1µs)* |
No |
No |
No |
Embeddable |
Yes (game/app integration) |
No |
No |
Partial |
|
Note
|
Honest status (ADR-0007):
† End-to-end encryption is part-way: no room is end-to-end encrypted today. See [_encryption_what_is_true_today]. |
Burble’s target is the one ADR-0011 sets out: the server has no capability to see call content, rather than a promise not to look. Burble is only part of the way there, and this section is the scoreboard.
What protects a call today. Every hop is encrypted with DTLS-SRTP. That is real encryption, and it is not end-to-end: it terminates at the SFU, so the server can decrypt the media it forwards. Where an older version of this file, a UI string, or a third-party summary says "E2EE", discount it.
What is implemented (server side, 2026-10-21). ADR-0016 moved Burble to peer-held group keys; the coordination half is in the tree:
-
Burble.Media.E2EEno longer generates or returns a room key. It stores, per room, public X25519 keys, an epoch counter, and opaque sealed blobs. There is no key-returning function in its API. -
Each peer would generate its own ephemeral keypair; one peer per epoch (the lowest peer id) generates the room key and seals it to every other peer under their pairwise X25519 + HKDF key. The server relays ciphertext it cannot open.
-
Pairwise agreement, sealing/opening and per-frame derivation are peer-side pure functions (
pairwise_key/6,seal_group_key/6,open_group_key/6,derive_frame_key/3) with domain-separated labels, AAD bound to room/epoch/direction, an all-zero-shared-secret check (RFC 7748 §6.1) and size caps. They are the part a test suite can exercise without a browser. -
The room channel exposes the coordination events a client needs —
e2ee_register_key,e2ee_key_set,e2ee_publish_sealed,e2ee_fetch_sealed,e2ee_status— so the protocol is reachable and not merely a library (docs/PROTOCOL.md). Every payload is a public key or an opaque blob. -
Any membership change bumps the epoch and drops every sealed blob, so a departing peer never learns the next key and a joining peer never learns a past one.
Burble.Security.KeyRotationnow requests these epoch bumps; it does not, and cannot, ratchet a key the server does not have.
The browser half exists, and is tested — it is not yet on the call path.
-
client/web/e2ee.jsis the peer side: X25519 keypairs, pairwise agreement, sealing, per-speaker frame keys, AES-GCM frame encryption, a replay window and epoch tracking. It is checked against the server’s own output by committed test vectors, so the two halves cannot drift apart silently (bun test client/web/tests/— 97 tests across 12 files, all green). -
client/web/e2ee-session.jsis the conversation that goes with it: it speaks the five channel events, elects the key-maker (lowest peer id), seals the room key to each peer, opens what arrives, and follows epoch bumps. It handles the two orderings that actually happen — a sealed key arriving before the key set, and one arriving from an epoch ahead of ours. -
client/web/e2ee.jsalso carries the Encoded Transform layer:FrameCryptor,senderTransform/receiverTransformandattachFrameCrypto(pc, …), which pipes them into aRTCPeerConnectionand reportsok: falsewhen the browser lackscreateEncodedStreams.
What is missing is the last mile, and it is the difference between "the protocol is implemented" and "your call is private":
-
Nothing calls them yet.
VoiceEngine.resdoes not build the session, does not attach the transforms to the peer connection, and has no key-exchange UI. So no call is encrypted beyond DTLS-SRTP, and the server can still decrypt what it forwards. -
It has never run in a browser. The tests exercise real WebCrypto and real
TransformStream`s, but with a fake peer connection shaped like Chrome’s API. `createEncodedStreamspresence, frame mutation in place, and the SFU’s fan-in are all modelled, not observed. -
There was already a client E2EE module, and it was not encryption.
client/web/src/app/voice/E2EE.resclaimed "AES-256-GCM with HKDF-SHA256" and XORed each frame with the local private key, repeated. Nothing imported it, it derived its key from one peer’s private key alone (so two peers could not have agreed), and the build artifact in a working tree is not even valid JavaScript. All three are luck rather than design. It is now a failing stub that points ate2ee.js. Had anyone wired it up, Burble would have shipped something labelled E2EE that is trivially breakable. -
The server-side coprocessor pipeline still runs in
:e2eerooms. It no longer receives a key (the engine refuses one and logs a warning) and cannot decrypt, but ADR-0011 §3 puts content DSP at the edge; that move is not done. -
No mid-epoch key-leader failover, and nothing on this path is formally verified.
Two design points the client work forced. The room key is shared, so on its own it authenticates the room, not the speaker — any member could forge frames that appear to come from any other. The frame layer therefore derives a per-sender key and binds the sender id into the AAD, and each frame states its speaker in its header: rewriting that header makes the receiver derive the wrong key, so the AAD check catches the forgery. A peer can still forge its own frames, which it is entitled to do; it cannot speak as someone else.
The second point is topology. Burble’s media plane is an SFU, so one receive path carries every participant; a frame layer that assumed "the remote peer of this connection" could not have worked. Frames carry their speaker and the replay window is per speaker, which is also what makes the same code correct in a mesh call.
So the names changed, to stop the drift. :e2ee_key became
:pipeline_key (server-side, explicitly not the room key); the pipeline
health map reports pipeline_encrypted:; the peer offer carries
e2ee_requested: — the policy asked for, not proof; Privacy.e2ee_config/1
returns enabled: false, implemented: false; and GET /api/v1/setup/check
reports e2ee_coordinator_running: instead of e2ee_keys_generated:. A room
marked :e2ee is a request until the client half lands.
Tracking: ADR-0016 §Gaps and the 2026-10-21 maintenance report (MNT-16).
Audio kernels — Zig remains the estate-policy implementation language. Historical direct-NIF microbenchmarks are retained below as provenance only; the direct loader has been retired. Production acceleration now requires proved ReleaseSafe WASM guests through SNIF, which are not yet shipped. The live call path forwards opaque Opus and does not currently traverse these kernels.
Precision Time Protocol — IEEE 1588 PTP clock source detection with graceful fallback (PTP hardware → phc2sys → NTP → system). Sub-microsecond accuracy when a PTP hardware clock is available; ~1ms via NTP on typical deployments. Jitter measurement, telemetry, and multi-node alignment data export built in. Direct in-VM device access is forbidden; a PTP hardware path requires an isolated OS service and real-hardware validation.
Four topology modes — From single-server (monarchic) to fully distributed
mesh. The serverless mode is designed for mandatory E2EE and will enforce it
once the client half ships; today no topology is end-to-end encrypted. Set
BURBLE_TOPOLOGY and go.
Erlang/OTP backbone — Supervision trees, hot code upgrades, fault isolation. If a room crashes, everything else keeps running. That’s not marketing — that’s OTP.
Bridge interop (experimental) — a Mumble/Murmur relay module exists but is not yet started by the supervision tree or validated against a real Murmur server. Jitsi and Matrix bridges are roadmap items, not code.
Embeddable client library — Drop Burble voice into your game, workspace, or app. Used in IDApTIK (asymmetric co-op game) and PanLL (panel workspace).
git clone --recurse-submodules https://github.com/hyperpolymath/burble && cd burble
just deploy # builds selur-compose if needed (~3 min first run), then brings up the stack
# Server: http://localhost:4000
# Web client: http://localhost:8080
# VeriSimDB: http://localhost:8081just deploy uses selur-compose — the TOML-native
Rust compose driver developed in-tree. No Python, no Docker Compose, no
podman-compose.
To stop the stack:
just down|
Note
|
--recurse-submodules is required. VeriSimDB is integrated as a
submodule under tools/nextgen-databases (paralleling tools/affinescript)
because Burble’s anti-purpose forbids vendoring it in-tree. If you already
cloned without it, run git submodule update --init --recursive — or just
run ./setup.sh, which now does that automatically.
|
|
Note
|
Building from source requires system packages for the quicer/msquic QUIC
transport library (cmake, perl, build tools, OpenSSL headers). See
CONTRIBUTING.md
for the full package list and per-distro install commands.
|
# Prerequisites: Elixir 1.17+, Zig 0.15+, Bun
# (plus system packages — see CONTRIBUTING.md)
# Test Zig kernel sources in the estate-required safety mode
cd ffi/zig && zig build test -Doptimize=ReleaseSafe
# Start server
cd server && mix deps.get && mix phx.server
# Start web client (separate terminal)
cd client/web && bun run devThe table below is historical direct-NIF benchmark provenance, not a current production claim. The direct NIF has been retired; equivalent measurements must be reproduced against proved SNIF guests before acceleration is described as available. The live SFU path forwards opaque Opus RTP and does not execute these kernels. End-to-end latency remains unbenchmarked (ADR-0007; issue #52).
| Operation | Elixir | Retired direct-NIF result | Historical speedup |
|---|---|---|---|
LZ4 compress |
83ms |
3.1us |
26,350x |
Echo cancel |
19.4ms |
310us |
62x |
FFT (256pt) |
826us |
22us |
37x |
Convolution |
300us |
11us |
27x |
Neural denoise |
54us |
9us |
6x |
If you’re curious about the tech behind these claims — how we hit 26,350x, what PTP actually does, why OTP gives us fault isolation — see EXPLAINME.adoc for the receipts.
-
Documentation Index — one-page map of every doc under
docs/and at the repo root. Start here if you don’t know where to look. -
Show Me The Receipts — feature highlights with code paths and honest caveats
-
Architecture — control plane, media plane, topology modes, supervision tree
-
Threat Model — security analysis and mitigations
-
ABI/FFI Design — Idris2 proofs + Zig implementation
-
Accessibility — accessibility features, compliance, and roadmap
-
Decision records (ADRs) — numbered, append-only design decisions
-
Wiki — landing + signposts (source in
docs/wikis/)
Open an issue on GitHub or reach out directly — happy to explain anything in more detail.
SPDX-License-Identifier: CC-BY-SA-4.0
Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
See LICENSE for full text.