It all runs in your browser — there's no app to install. Your video is encrypted on your device before it ever leaves, so:
Broadcasting needs a publish key — free, about a minute, and it asks you for nothing. Watching needs only a link. How you're protected →
Two browsers, our Worker, one CDN. Notice that the content key lives only in the share link and is derived on each device — nothing in the middle can decrypt your video.
#k=) that will live only in the link.CK = HKDF(#k=, salts) — computed in the browser, sent nowhere.#k= from the URL.#k= — proof they hold the whole link — and receives a subscribe token of their own.CK, independently, in the browser.Everything in the path is one of these. Only two of them are secrets.
publish keyAdmits you to broadcast. Carries its own expiry under a signature we can't forge and isn't stored anywhere, so it links you to nothing. It can't decrypt. Request one.
route tagProof you were given the link, derived from #k= with different inputs than the content key. It's what a viewer shows to be placed on a relay. Decrypts nothing.
node idAn Ed25519 public key (base32). The stream's identity and the relay track name. Public.
salts + epochPublic HKDF inputs (a global kill-switch salt ‖ a per-stream salt). Rotating one re-keys the stream.
JWTA short-lived, per-broadcast CDN token that authorizes the connection (publish or subscribe scope), signed by our Worker over one broadcast path. Not a content key.
#k= → CKThe secret. 32 bytes in the link fragment (never sent to a server) → AES-256-GCM key via HKDF. Held only by the two browsers. Chat is sealed under a sibling key from the same bytes and a different HKDF info.
passcodeOptional second secret. Deliberately kept out of the link — spoken or texted, stretched with PBKDF2 and mixed into CK. Never sent to or checked by any server.
Every party in the middle is content-blind. What we do learn is that a broadcast
happened, its name, and roughly how many sessions watched it — the price of being able to
stop one. There is no sign-in on this site, and the accounts tier that exists in the
repository is switched off; /api/config reports it so you can check.
| Party | Can see | Never sees |
|---|---|---|
| Our Worker (earthseed.live) | that a broadcast started and ended, its name and route tag, its settings, counts of viewing sessions, chat ciphertext | your #k=, the key CK, your passcode, your video, audio or messages — or who you are |
| The CDN (cdn.moq.pro) | a connection token, encrypted (unreadable) frames, the catalog (codec/resolution), your IP | your #k=, the key CK, your video & audio |
| Someone with the link | your video, audio & chat — the link carries #k=, so anyone with it can watch (share it carefully) | can't publish as you, rewrite your settings, or rotate your key |
| Someone without the link | at most encrypted, unreadable frames — plus the cleartext catalog (codec/resolution) and traffic size/timing | your video, audio, or key — nothing decryptable |
A relay never holds a key, so which company runs it is not what protects your video — the encryption is. But it is worth saying plainly what changed.
Media rides through moq.pro, a CDN we do not operate. Our Worker mints a per-broadcast Ed25519 token naming one account root and exactly one broadcast path beneath it, with an expiry. moq.pro holds only the public half of that key: it can verify a token and cannot forge one.
This page used to describe something stronger, and it was true at the time. We ran the relays ourselves: single-tenant Hermit unikernels, one broadcast per machine, no shell to reach and no persistent disk, isolated by a hardware-virtualisation boundary rather than by namespaces on a shared kernel. That argument no longer describes the infrastructure, so it has been removed rather than left standing next to something it does not fit. Anyone self-hosting can still take that path — the code for it is in the repository.
What that costs, honestly. Broadcasts are no longer separated by a machine boundary; a CDN is shared infrastructure. What still scopes a compromise is that a token names one broadcast and nothing else — and, far more importantly, that a relay only ever holds ciphertext. The machine boundary was defence in depth, not the defence. We also have less to tell you about the operator, because we are no longer the operator. TRUST.md has the rest.
Our own code, unminified and unbundled. The transport is a library we didn't write and didn't change.
simple/
— thirteen unminified, documented files, about 7,500 lines, that run in the browser as-is
with no build step and no runtime dependency of our own. Capture, composite, encode,
encrypt, decrypt, decode, render.
This used to say one file, and that was a real thing to be able to say. It is
no longer true, so what stands in its place is
INTEGRITY.md:
a SHA-256 for every file above, committed to git so the record lives under a different
party than the site serving it. npm run verify compares the two.
@moq/net
(Media over QUIC), by Luke Curley.
It is unmodified, but we now serve
our own copy
of it instead of loading it from someone else's CDN. Here's why, and how to check us.
The transport runs on the same page as your content key. Whoever serves that code could swap it for something that reads your key straight out of the link and mails it elsewhere — no bug on our side required. So the only version of it we're willing to run is one we serve ourselves, fixed at deploy time and reviewable in the repo.
Pinning a version on someone else's CDN is not the same thing, which is easy to
miss. A pin names a version, not the bytes behind it — and it only covers the package you
named. @moq/net pulls in four more (@moq/qmux,
@moq/signals, async-mutex, zod) as version
ranges, which a CDN resolves fresh on every request. Pin the top package and a new
release of any of the four still starts running on this page, unchanged on your side.
So we build it once from exact versions and serve it from this origin. Nothing about it is
modified, and you shouldn't take our word for that — the build is reproducible.
The recipe
is four commands; run them and you should get a file whose SHA-256 matches
the one we ship,
which you can also compare against
Luke's upstream source.
It's left unminified so it stays readable. Full reasoning and the honest limits are in
TRUST.md.
No build step.
Download a copy
and serve it from your own domain. HOSTING.md covers verifying it, and what
it doesn't change.
Encryption this strong has a cost, and it is worth stating rather than discovering.
Because nothing in the middle can decrypt anything, we can't police content — so we don't pretend to review it. What we can do is stop a broadcast: any viewer can report one from the watch page, and an operator decides. Nothing is automatic; "enough reports" would be a weapon aimed at exactly the people this exists to protect.
Terminating means no further relay placement and no further token for that name. Browsers running our client stop within seconds; a session already open ends when its relay token expires and is not reissued. We still can't say what it contained.
The short version: your video is encrypted on your device with a key that lives only in
the # part of your link, which browsers never send to a server. There is no
account, no email and no password. A publish key admits you to broadcast and is never
stored, so nothing connects you to what you broadcast. A viewer must prove they hold
your link before a relay will even be assigned to them — knowing your stream's name is
not enough.
And the limits, in the same place as the promises: a link can't be recalled, a viewer can always record, and your IP is visible to the network. Read the whole thing →