Free · ISC · Rust sender · Flutter receiver
The QR codes are the wire.
Porter moves a file between two machines that share no network. A terminal
displays it as a slideshow of QR codes, a phone camera reads them back, and
fountain coding means the receiver never has to catch any particular frame.
cd rust-sender
cargo build --release
./target/release/porter-sender myfile.pdf --fountain
# Point the receiver app at the terminal. That's the whole protocol.
A link with no back-channel
The receiver cannot ask for a frame again. A dropped frame — bad angle, motion blur, a reflection at exactly the wrong moment — is simply gone, and every interesting decision in Porter follows from that.
- SequentialFrame n is byte range n. Miss one and you wait for the sender to loop all the way back to that index.
- FountainEach frame XORs a random subset of blocks, picked from its sequence number by a PRNG both sides run. Any sufficient pile rebuilds the file.
- So nothing is specialNo individual frame matters, which means a persistently missed one costs nothing at all.
One binary on the sending machine
Porter is an air-gapped tool, so the sending machine is often not the one you keep a runtime on. The sender compiles to a single static binary — no Node.js, no Python, nothing to install alongside it.
- porter-sender <file>A ratatui TUI — QR grid, sidebar, scrubbing and gap-fill. Runs on the offline machine.
- Receiver appFlutter, on Android and macOS. Scans, decodes, verifies, and resumes from disk if it is killed.
- porter-sender serveAn HTTP receiver on axum, for when a network turns out to exist after all.
- porter-sender joinReassembles the .partaa and .partab files a receiver wrote, with a SHA-256 check.
What Porter does not do
The security story is short, which is the point. No account, no server, no telemetry, and by default no trace left on disk.
- No network pathThe sender opens no sockets on the QR path. The transport is a camera pointed at a screen, so there is nothing to intercept in transit.
- No disk trace by defaultSlideshow position persists only under --resume. Without the flag, nothing is written.
- No telemetryNo analytics, no crash reporting, no phone-home. Both binaries do exactly what the flags say.
- Verified on arrivalFountain transfers always carry a CHECKSUM frame. The receiver checks SHA-256 before saving.
- Resumable receiverChunks land on disk as they decode, so a killed app rehydrates from the files rather than re-scanning.
- Decisions on the recordThe worker isolate, fountain coding, disk hydration and the Rust move are all written up as ADRs.
Honest limits, and an ISC licence
QR is a slow, lossy, line-of-sight link, and no amount of coding changes that. A 109 MB file is roughly 26,000 chunks — a real transfer that has been run end to end, and also a long time holding a phone at a screen. Porter is for the case where two machines genuinely cannot talk - no NIC, an isolated network, a device you will not plug anything into.