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, one broker, a relay fleet. 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.

Broker · tinymoq.com gates connections · serves public salts 🔒 content-blind — never sees your key ③ you send the share link watch.html?node=…&o=… #k=the content key (secret) broadcaster browser ① mint node id — Ed25519 public key ② mint #k= — 32 random bytes CK = HKDF(#k=, salts) ⑥ encode → AES-256-GCM encrypt camera → encode → encrypt → send viewer browser ⑦ read node · origin · #k= from link CK = HKDF(#k=, salts) decrypt → decode → play receive → decrypt → decode → play Origin relay encrypted · can't decode Edge relay encrypted · can't decode ④ assign publish (publish key) · PUT salt ← origin EID + publish JWT ⑧⑨ get salt · assign watch ← edge relay + subscribe JWT ⑥ WebTransport ?jwt iroh pull ⑪ 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. 4Assign + salt. The broker gives a gated relay + short-lived token and public salts.
  5. 5Derive key. CK = HKDF(#k=, salts) — computed in the browser, sent nowhere.
  6. 6Encrypt + publish. Each frame is AES-256-GCM encrypted, then sent to the origin relay.
  7. 7Open link. The viewer reads the node, origin, and #k= from the URL.
  8. 8Salt + edge. The viewer fetches the salts and a gated edge relay from the broker.
  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 one of them is a secret.

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.

origin EID

Which relay holds the origin, so a viewer's edge knows where to pull from. Routing only; 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 relay token that authorizes the connection (publish or subscribe scope). 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.

Who can see what

The design makes every party in the middle either content-blind or removable.

PartyCan seeNever sees
Broker (tinymoq.com)node id, public salts, coarse geo; places you on a relayyour #k=, the key CK, your video & audio
Relay fleeta connection token, encrypted (unreadable) frames, the catalog (codec/resolution)your #k=, the key CK, your video & audio
Someone with the linkyour video & audio — the link carries #k=, so anyone with it can watch (share it carefully)can't publish as you 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

The relays are unikernels, not containers

Security you can't switch off beats security you have to remember to switch on.

A relay never holds a key, so this isn't what protects your video — the encryption is. It decides something else: how much an attacker gets if a relay falls, and how much has to keep going right for that to stay true.

The ordinary way to run this is containers on Kubernetes, and Kubernetes can be locked down well. The trouble is that its security is a set of policies someone has to choose, apply, and keep applying — and each one has an off switch. kubectl exec hands you a shell inside a running workload, gated only by a permission someone can grant. Containers share one kernel, so a kernel bug is an escape, and the settings that stop a container mounting the host are enforced only if an admission policy says so. Without a network policy, every workload can reach every other. The container image itself ships a package manager and a shell, so the post-exploitation toolkit arrives with the app.

Our relays are Hermit unikernels: a single program in a single address space, booted directly under a hardware-virtualisation boundary. There is no shell to reach — not disabled, absent. No package manager, no filesystem, no users, no second program to pivot to, and no persistent disk; idle relays are reaped and the machine stops existing. None of that is a policy anyone can relax later, which is the whole point: a hardened cluster is a state you must maintain against drift and deadlines, while a unikernel's hardening is a property of the artifact.

One relay carries exactly one stream. Many viewers of that stream share it — that's what a relay is for — but two different broadcasts are never put on the same machine, anywhere. So a compromised relay is confined to a single broadcast by a machine boundary rather than by a setting. Your access token names one broadcast as well, which means even sharing a relay would grant nothing: two independent barriers, not one. And the fleet is given only the public half of the signing key — it can check a token, it cannot forge one.

Where the argument stops. The machine hosting those unikernels is still an ordinary Linux box running a fleet manager — "nothing to log to" is true of the relay, not of the system around it, and that host is the real target. A smaller ecosystem also means fewer eyes and slower patching than the Linux container world. And a unikernel bounds the damage from a bug in the relay — it doesn't make the relay correct. TRUST.md has the full comparison.

What runs in your browser

Our client is one file. The transport is a library we didn't write and didn't change.

Our whole client

earthseed.js — one unminified, documented file that runs in the browser as-is (no build step). It does capture, encode, encrypt, decrypt, decode and render. What you read is what runs.

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

Six files, 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 →