earthseed.live End-to-end encrypted

Secure live streaming,
with maximum privacy protection

It all runs in your browser — there's no app to install. Your video is encrypted on your device before it ever leaves, so:

🌐The browser executes 100% open source software (copy it).
📡The relay carries your video but can't decode it — token protected, no operator access.
🔗The broker connects you to a relay. Your video never passes through it, and it keeps no record of your stream.
Broadcast Get a publish key Read the code

Broadcasting needs a publish key — free, about a minute, and it asks you for nothing. Watching needs only a link. How you're protected →

How a stream travels — and what's exchanged

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.

Our Worker · earthseed.live admits publishers · mints CDN tokens · can refuse 🔒 content-blind — never sees your key ③ you send the share link watch.html?node=… #k=the content key (secret) broadcaster browser ① mint node id — Ed25519 public key ② mint #k= — 32 random bytes ⑤ CK = HKDF(#k=, salts) ⑥ composite → encode → AES-256-GCM camera + screen → encode → encrypt → send viewer browser ⑦ read node · #k= from link ⑩ CK = HKDF(#k=, salts) ⑪ decrypt → decode → play receive → decrypt → decode → play cdn.moq.pro encrypted · can't decode ④ prove the name (signature + publish key) ← CDN URL + publish JWT ⑧⑨ present route tag ← CDN URL + subscribe JWT ⑥ WebTransport ?jwt ⑪ WebTransport ?jwt your video travels encrypted end to end · only the codec & size are visible
  1. 1Identity. The browser mints an Ed25519 keypair; the public key is the stream's name.
  2. 2Content key seed. 32 random bytes (#k=) that will live only in the link.
  3. 3Share. You hand someone the link — that (and only that) lets them watch.
  4. 4Prove the name, get placed. The browser signs a challenge with the key the stream is named after; our Worker checks the publish key and mints a short-lived CDN token.
  5. 5Derive key. CK = HKDF(#k=, salts) — computed in the browser, sent nowhere.
  6. 6Encrypt + publish. Camera and screen are composited into one frame, encoded, AES-256-GCM encrypted, then sent to the CDN. The salts ride along on the catalog track, so a viewer never has to ask us for them.
  7. 7Open link. The viewer reads the node id and #k= from the URL.
  8. 8Prove the link, get placed. The viewer derives a route tag from #k= — proof they hold the whole link — and receives a subscribe token of their own.
  9. 10Derive same key. Same inputs → same CK, independently, in the browser.
  10. 11Decrypt + play. Ciphertext in, video out — only here and on the broadcaster.

The values being exchanged

Everything in the path is one of these. Only two of them are secrets.

publish key

Admits 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 tag

Proof 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 id

An Ed25519 public key (base32). The stream's identity and the relay track name. Public.

salts + epoch

Public HKDF inputs (a global kill-switch salt ‖ a per-stream salt). Rotating one re-keys the stream.

JWT

A 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= → CK

The 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.

passcode

Optional 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.

Who can see what

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.

PartyCan seeNever sees
Our Worker (earthseed.live)that a broadcast started and ended, its name and route tag, its settings, counts of viewing sessions, chat ciphertextyour #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 IPyour #k=, the key CK, your video & audio
Someone with the linkyour 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 linkat most encrypted, unreadable frames — plus the cleartext catalog (codec/resolution) and traffic size/timingyour video, audio, or key — nothing decryptable

Where your video actually goes

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.

What runs in your browser

Our own code, unminified and unbundled. The transport is a library we didn't write and didn't change.

Our client

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.

The transport

@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.

Why we serve our own copy instead of linking to a CDN

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.

Host your own client code

No build step. Download a copy and serve it from your own domain. HOSTING.md covers verifying it, and what it doesn't change.

What happens when something goes wrong

Encryption this strong has a cost, and it is worth stating rather than discovering.

We can stop a stream. We cannot watch one.

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.

How you're protected

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 →