Send a file straight from one device to another.
A QR code carries the key; the file goes over WebRTC;
nothing in between ever holds a readable copy.
share.stan-ely.com — no install, no account, no upload.
npx qrdrop send report.pdf # prints a QR code in your terminal
npx qrdrop receive # on the other machine![]() |
![]() |
![]() |
|---|---|---|
| Show the code | Match the words | Beam, for no network |
Nothing to install for the browser: open share.stan-ely.com on both devices. Everything else, by where you are:
| Any machine with Node 22+ | npx qrdrop send report.pdf and npx qrdrop receive — the QR is printed in the terminal |
CLI on PATH |
brew install stan-ely/tap/qrdrop · scoop install stan-ely/qrdrop (after scoop bucket add stan-ely https://github.com/stan-ely/scoop-bucket) · npm i -g qrdrop |
| Browser UI, run locally | npx qrdrop web — serves this copy on 127.0.0.1 |
| Windows app | Microsoft Store · scoop install stan-ely/qrdrop-app · Releases |
| macOS app (Apple silicon) | brew install --cask stan-ely/tap/qrdrop-app · Releases |
| Android app | F-Droid repository (this project's own, same signed APK) · Releases · Google Play is in closed testing — join as a tester |
| Linux app | .deb and AppImage on Releases — Beam only; the webview has no WebRTC |
A file sent from the CLI can be received in a browser, and the other way round. That interoperability is the reason this is one package rather than three.
| Where | How |
|---|---|
| Browser | share.stan-ely.com, or <qr-drop> in a page of your own |
| Terminal | npx qrdrop send / receive — the QR is printed as text |
| Library | import { openRoom, sendFile, createReceiver } from 'qrdrop' |
| Air-gapped | Beam — animated QR on one screen, a camera on the other |
| App | A desktop and mobile shell — a real file picker, the system camera, and scanned links that open it |
- One protocol, three runtimes. The same
src/core/runs in a browser, in Node and in a Tauri app, and is typechecked twice — with and without Node's globals — so "isomorphic" is a build failure rather than a promise. Protocol - Keys from a QR code, checked by eye. 32 random bytes in the code, HKDF for everything else, an ephemeral ECDH per session, per-chunk AES-GCM on top of WebRTC's own encryption, and a four-emoji SAS that gets exactly one attempt per secret. WebCrypto only. Protocol · Threat model
- A fountain code for no network at all. Beam animates LT-coded QR frames, so a receiver needs enough frames rather than particular ones — 1.48 × N frames at 10% loss, against 8.3 × N for numbered chunks on a loop. Beam
- No framework, four runtime dependencies. The UI is a custom element over a hand-rolled virtual DOM, laid out to never scroll, and checked by a script that walks every screen in Chromium and Firefox, at four viewports, with a mouse and with touch.
- Shipped where people are. npm with provenance, Homebrew, Scoop, the Microsoft Store, and a self-hosted F-Droid repository whose APK is built reproducibly — two builds in different directories are byte-identical. The app
Two devices in the same room. One shows a QR code, the other points a camera at it, and that scan is the one channel an attacker cannot stand in the middle of.
The QR carries 32 bytes of CSPRNG output. Everything else derives from it:
| Derivation | Purpose |
|---|---|
HKDF(secret, "topic") |
Trystero room ID — what peers meet on |
HKDF(secret, "signal") |
Trystero password, encrypting session descriptions |
HKDF(ECDH, salt=secret, "host->guest") |
file bytes, sender to receiver |
HKDF(ECDH, salt=secret, "guest->host") |
file bytes, the other way |
HKDF(ECDH, salt=secret, "sas") |
the four emoji shown on both screens |
The room ID is derived rather than being the secret itself. Using the secret as the room name is the obvious shortcut, works perfectly in testing, and silently publishes your key to every relay on the network.
Both devices then show the same four emoji, and the sender confirms they match before the filename or size is sent. The receiver's Accept is the only other gesture. Why each of those steps is there, and what breaks without it, is in docs/protocol.md.
Protected: relays and TURN servers see ciphertext, timing and volume only; a network attacker without the QR cannot join, read or tamper with a transfer; past transfers stay closed if a code leaks afterwards; truncated, reordered or altered files are rejected rather than written.
Not protected: anyone holding the code can join — it is the whole credential, so show it to a person and not to a room or a screen share; both peers learn each other's IP address; the host serving the page could serve modified code; relays see that two keys met.
Warning
Beam is not encrypted, and cannot be. There is no handshake on a one-way channel. It is for air-gapped machines, where the alternative is a USB stick.
The full version, including how long a code stays useful and what the route indicator can and cannot tell you, is docs/threat-model.md. Vulnerabilities go privately through SECURITY.md.
npm install
npm test # unit suite, offline
npm run typecheck # two tsc runs, browser and Node; nothing is compiled
npm run build # esbuild -> site/dist/
npm start # build, then serve site/dist/ on :4173localhost counts as a secure context, so WebCrypto and the camera both work
against npm start without a certificate. The end-to-end suites
(npm run test:e2e, npm run test:e2e:interop) need a network and public Nostr
relays, so they stay out of npm test and out of CI.
CONTRIBUTING.md has the house style, the source layout, and
the rules a change can break silently.
| Protocol | Pairing, key derivation, framing, the transport seam, sources and sinks |
| Threat model | What is and is not protected, and known limitations |
| Beam | The no-network mode: the fountain code, its measurements, and prior art |
| The app | Desktop and mobile installs, what works on each platform, signing |
| Hosting | Running the site bundle yourself, and how share.stan-ely.com is deployed |
| app/CAPABILITIES.md | The app's lab notebook: every platform measurement, phase by phase |
| CHANGELOG.md · app/CHANGELOG.md | Release notes for the npm package and the app |
MIT.



