Skip to content

Repository files navigation

Universal Beam

Send text and files straight between your devices. Pair two browsers with a short code, then type, paste or drop a file — notes, links, photos, anything. It goes device to device over a WebRTC data channel, encrypted end to end by the browser, and no server ever holds a byte of it.

Part of the UNI·SIM Universal Apps — free, open source, no account required. Served at opensource.unisim.co.uk/beam.


What it is, in one diagram

  browser A ──wss──┐                     ┌──wss── browser B
                   ├─ rendezvous room ───┤        (a Durable Object in
                   │  (SDP + ICE only)   │         backoffice/opensource-portal,
  browser A ═══════╪══ RTCDataChannel ═══╪═══════  one instance per code)
                   │      (your text)    │

The rendezvous carries the negotiation and nothing else. Once the data channel opens, Beam closes the WebSocket — so the server is not merely trusted to stay out of the payload path, it is removed from it. There is an end-to-end test that asserts exactly that (e2e/beam.spec.ts).

What it deliberately cannot do

These are on the page in the app itself, not just here. Each is a browser limit or a deliberate trade, and each has bitten someone who assumed otherwise.

Works offline No. Two devices on the same Wi-Fi still need the internet to pair. A browser tab cannot browse mDNS and cannot join a multicast group — there is no web API for either — so it cannot discover anything on your LAN. If you need transfers with no internet at all, use LocalSend; it is free, and it is better at that than a web page can be.
Finds nearby devices automatically ❌ No. Explicit code + QR only. The Snapdrop/PairDrop model buckets sockets by public IP, which shows strangers to each other on carrier-grade NAT and cannot work across networks at all.
Mirrors your clipboard ❌ No, and it never will. navigator.clipboard.readText() does not exist in Firefox and is permission- plus gesture-gated elsewhere. Sending is always something you press.
Falls back to a relay when direct fails ❌ No — direct-or-fail. There is no paid TURN. Where a network blocks peer-to-peer, Beam says so in plain words with a reason and something to try (see src/lib/diagnose.ts). A silent spinner is the worst possible outcome for this product.
Remembers anything ❌ No account, no history, no sync, no "send to a device that's switched off". Closing the tab is the delete button.
Sends files of any size, in any browser ⚠️ Depends on the receiving browser. Chrome/Edge stream to disk as the bytes arrive (File System Access API), so size barely matters. Safari and Firefox have no streaming save: the whole file sits in memory until it becomes a download, and their practical ceiling is free RAM. The offer row warns when that risk is real (MEMORY_WARN_BYTES).
Resumes an interrupted transfer ❌ No. A dropped connection fails the transfer with a sentence; send it again. Resume is v3-that-will-probably-never-happen (next-products.md §13.4).

Sentences that must never appear in this app's copy (each is false on a leg the product actually supports): "never leaves your network", "no servers involved", "works offline". There is an e2e test asserting the page does not contain them.

Develop

cd D:/Github/UNISIM/Universal_Apps/Universal_Beam    # macOS: ~/Github/UNISIM/...
npm install
./scripts/preview.sh        # or  .\scripts\preview.ps1   on Windows

Port 5197, reserved for Beam in Docs_UNI_SIM/dev-preview.md. The scripts pass --strictPort, so a clash fails loudly rather than serving Beam on another app's port.

Pairing needs the internet even in dev. The dev server serves the UI, but the two tabs still find each other through the live rendezvous at opensource.unisim.co.uk/rtc/room. Point that elsewhere with VITE_RENDEZVOUS_ORIGIN (e.g. a local wrangler dev of opensource-portal).

A red CORS error in the dev console is expected. /rtc/turn sets no CORS headers, and in dev the app is cross-origin to it. The fetch fails, Beam falls back to public STUN — which is exactly what that endpoint returns today — so dev and prod behave identically. Do not "fix" it in opensource-portal.

Command Does
npm run dev Vite dev server
npm run build tsc -b then a production bundle into dist/
npm run lint ESLint (flat config, typescript-eslint)
npm test Vitest — the pure logic (code minting, failure diagnosis)
npm run test:e2e Playwright — two real browsers, the real rendezvous, real text over a real data channel. Needs an internet connection.

npm run test:e2e is the one that matters. Everything else can be green while the product does not work.

Architecture

File Job
src/lib/rtc.ts BeamSession — joins the room, tie-breaks who offers, runs ICE, opens the data channel, hangs up on the rendezvous, diagnoses failure.
src/lib/files.ts FileTransferEngine — the file protocol over that channel: offer/accept, 64 KiB chunks, real backpressure (bufferedamountlow, never a sleep), queue, cancel from either end. DOM-free, so the whole protocol is unit-tested against a fake channel.
src/lib/fileSink.ts Where received bytes go: streamed to disk on Chrome/Edge, memory + a download elsewhere — and the honesty about the difference.
src/lib/sas.ts The safety number — derived from both DTLS fingerprints, same on both screens unless someone is in the middle.
src/lib/code.ts Mints the pairing code. The code is the only authentication this product has — 6 glyphs from a 32-glyph alphabet via crypto.getRandomValues (2³⁰ ≈ 1.07 billion), never Math.random().
src/lib/diagnose.ts Turns collected ICE evidence into a sentence a person can act on.
src/lib/clipboard.ts Copy/paste, and the honest limits of both.
src/stores/beamStore.ts Zustand store; owns the one live BeamSession.
src/components/ UI. PairCard (code + QR + join), Room (composer + file drop + session timeline), FailureCard, Honesty.

The file protocol, in one paragraph

Control frames are JSON strings; file bytes are raw binary messages on the same ordered channel, so a chunk can only belong to the one transfer that is currently accepted — that single invariant is why chunks go unlabelled. Each side sends at most one file at a time (extra sends queue); both directions at once is fine. Files are direct-or-fail like everything else: no relay, no resume, a failure is a sentence. Nothing about a file ever touches the rendezvous — it is long gone by then.

The pairing protocol

Room control frames from the Durable Object carry type (waiting / paired / peer-left); everything a peer sends carries t (hello / sdp / ice) and is relayed verbatim. Keeping those namespaces apart is the difference between "the room told me my peer left" and "my peer sent me the word peer-left".

Who makes the offer is decided by a random 32-bit nonce exchanged in hello, not by the role in the URL — because a user can open the same join link on both devices, and two "guests" would otherwise wait for each other forever.

Security

  • The room code is the lock. The rendezvous authenticates nobody; whoever joins first becomes your peer, and your browser will faithfully verify the DTLS fingerprint they published. The code is minted with crypto.getRandomValues and is 6 characters for that reason.
  • The room caps at two peers, so an attacker must win a race to be second, not merely guess eventually.
  • DTLS is mandatory-to-use in WebRTC. The data channel is encrypted whether or not we do anything, with per-session keys.
  • The safety number (SAS). Once connected, both screens show a six-digit number derived from the two DTLS certificate fingerprints (src/lib/sas.ts). A rendezvous server playing man-in-the-middle has to run two DTLS sessions with two different certificates, so the two screens cannot agree. Display-only — a check a person can make before sending something sensitive, not a gate. This is what makes "end-to-end encrypted" defensible against a malicious signalling server rather than merely true in the happy path.

Licence

MIT — see LICENSE.

About

Universal Beam — send text and links straight from one device to another, browser to browser, with nothing stored on a server. Part of the Universal Apps suite.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages