BURNERROOM/Security
How this actually works

What protects a conversation in BurnerRoom

A plain-English walkthrough of the encryption and infrastructure controls in the running code. Every claim below describes what the deployed code does today — not what is planned, and not what the underlying library is capable of. Where a protection is weaker than you might assume, it is written down in Known limitations rather than left out.

Reflects commit f16f2305 · 2026-08-10
Encryption
AES-256-GCM
one room key per generation, fresh IV per message
Message bodies
Ciphertext only
message text never leaves your device unencrypted
Room lifetime
Your choice
timed up to 24h, or no expiry at all
Accounts
None
no sign-up, no password, no email
Abuse control
20 msg / sec
per connection, enforced server-side
Call relay credentials
~1 hour lifetime
generated fresh, never a fixed secret
Encryption Verified

Messages are encrypted before they leave your device

Every message is encrypted in your browser and stays encrypted for its entire journey. The key is derived from the secret half of your room key — a value that is never sent to us — so the encryption happens entirely on your device, and ours only ever handles the result.

Today that is a single AES-256-GCM key per room, held for as long as that room's key generation lasts, with a fresh random initialisation vector for every message. It is not a new key per message, and it does not give you forward secrecy: someone who obtains a room key can read every message sent in that room under that key. Rotating the key generation — which happens when you revoke a guest — is what puts a hard boundary between an old key and everything after it.

Primitives: HKDF-SHA256 derivation from the room secret, AES-256-GCM with a 96-bit random IV per message, non-extractable keys via the WebCrypto API — chat/client/core/keys.js, chat/client/core/comm.js
Transport Partial

Messages reach the other person through our relay

Encrypted messages travel over a WebSocket connection to our server, which stores the encrypted blob and forwards it to the other participant. We hold ciphertext and never the text itself — but we do handle every message, and we can see the room identifier, message sizes, and when messages are sent.

The app also opens a WebRTC connection for voice and video. That connection is deliberately forced through a relay rather than joining the two browsers directly, so neither participant learns the other's IP address. It also means the relay provider carries that traffic. A direct browser-to-browser path for text exists in the codebase but is not active in the deployed app — see Known limitations.

Path: WebSocket relay through a Cloudflare Durable Object; WebRTC with relay-forced ICE transport for calls — chat/client/core/comm.js, chat/src/room-do.js
Room access Verified

Half of your room key never leaves your device

A room key is two independent halves. The first half is what the server checks you in with, and it only ever stores a one-way hash of it, compared in constant time so it can't leak a partial match through timing. The second half is what every encryption key in your conversation is derived from, and once your key is in your hands, that half is never transmitted to us again.

There is one exception, and it is worth stating plainly. Your Owner Key is generated on our server at the moment of purchase and held in encrypted form so that we can hand it to you. That copy is deleted the instant you collect it. If you never collect it, it stops working exactly 24 hours after your payment completes, and the hourly sweep that erases it puts the outside limit at 25 hours. For at most those 25 hours — and only then — we hold your complete key in a form we could decrypt. After that, we cannot: what remains is a one-way hash of the check-in half, and nothing that can reconstruct the secret half.

A Guest Key carries the room's shared secret inside it, which is how two people reach the same encryption key without that secret passing through us. A room has one guest at a time. Revoking that guest rotates the room's secret outright, so a key you have withdrawn cannot decrypt anything that comes after it, even in the hands of someone who kept a copy of the string.

Scheme: BR1 split key (128-bit auth half, 128-bit secret half, Crockford base32), HMAC-SHA256 server-side, HKDF-SHA256 client-side — chat/client/core/keys.js, web/src/index.js
Storage Verified

What we hold, and for how long

Message bodies are stored only as the encrypted blob your browser produced, opaque to us either way. Stored messages expire automatically after 7 days, are capped at 5,000 per room, and a server-side alarm removes them on that schedule whether or not anyone reopens the room. Revoking a guest deletes them immediately, as part of the same operation.

Alongside the ciphertext we hold operational data that is not encrypted to you: the room identifier, connection slots, push notification subscriptions, and a hash used to admit connections to the room. This is what lets the service route messages at all, and it is described in the privacy policy.

Limits: 7-day message TTL with a server-side backstop sweep, 5,000 stored messages per room max — chat/src/room-do.js
Ephemerality Verified

Rooms end, and take the conversation with them

A room ends in one of three ways: you give it a lifetime and that deadline arrives, you destroy it, or you revoke your guest. Any of the three deletes the conversation from our servers. A lifetime can be set when you open a room and extended later in 5/15/30-minute or 1-hour steps, up to a 24-hour ceiling, and that deadline is a real backend alarm — not a timer that stops counting when a tab closes. When it fires, every connection is force-closed and the room's access hash, keys and stored messages are wiped in a single delete.

A lifetime is optional. If you choose no expiry, the room stays available until you destroy it or revoke your guest. Stored messages in a never-expiring room are still swept after 7 days by a separate backstop, so an abandoned room does not keep its contents indefinitely.

Ending a room also tells the other participant's browser to erase its saved copy, and it does — provided their device is reachable at the time. See Known limitations.

Mechanism: Durable Object alarm() → full delete on room expiry; server-side purge on revoke; prefix-scoped sweep of stored messages on the 7-day backstop — chat/src/room-do.js, web/src/index.js
Identity Verified

No accounts, no password database, nothing to breach

There's no sign-up, no username, no password, and we never ask for your email address. Access to a room is the key itself: you hold it, we hold a one-way hash of half of it. The only "identity" involved in a session is a random token your browser invents for itself, and it disappears along with everything else when the room ends. There's no user table anywhere to leak.

Payment is handled by Stripe, which keeps its own record of the transaction under its own terms. We keep a record of the payment itself — amount, currency, status and Stripe's reference — because we're required to, and nothing that identifies you personally.

Notifications Verified

Push notifications carry no message content

If you turn on notifications, what travels is a bare category label — "new message," "reaction," and so on — plus the room identifier. Never the message itself. The text you see in the notification is a fixed phrase your own device fills in locally.

Push notifications are delivered by your browser vendor's push service — Google, Apple or Mozilla, depending on the browser. The payload is encrypted to your device, so that service cannot read even the category label or the room identifier. It can see that a notification was sent to your subscription, and when.

Payload: {type, room}, encrypted to the subscriber — chat/src/room-do.js, chat/client/service-worker.js
Calling Verified

Relay credentials expire in about an hour

Voice and video use short-lived TURN relay credentials generated fresh for each session — not a fixed secret shipped in the app. There's nothing long-lived to extract from the client even if someone tried.

TTL: ~3,600 seconds, generated server-side per request
What this doesn't cover yet

Known limitations