Wire protocol · v1

What's actually inside the QR codes.

Last updated: 2026-05-01

transfqr is a one-way protocol: the sender's screen flickers QR frames; the receiver's camera grabs whichever ones it can. There's no back-channel — no Bluetooth handshake, no WebRTC negotiation, no acknowledgements going the other way. The trick that makes this work is fountain coding. Any K + ε received frames reconstruct the file, no matter which ones you missed.

Goals

Frame envelope (18-byte header)

Every QR code carries one frame. The frame is a binary blob, base45-encoded (RFC 9285) so the QR encoder picks the dense alphanumeric mode — ~60% more bytes per QR than the byte-mode base64 alternative. All integers little-endian.

BytesFieldTypeNotes
0..1magicu160x5451 ('TQ')
2versionu8currently 1
3packetTypeu80x01 = meta, 0x02 = data
4..7fileIdu32random per session; receiver discards on mismatch
8..11totalChunksu32K = number of source blocks
12..13blockSizeu16bytes per source block
14..17seedu32LT PRNG seed (data only; meta sets to 0)
18..payloadtype-specific

Packet types

Meta (0x01)

Sent periodically by the sender (every 10th frame) so a receiver joining mid-stream can bootstrap. The meta packet carries:

fileNameLen   u8
fileName      utf-8 bytes
mimeLen       u8
mimeType      utf-8 bytes
originalSize  u64       (8 bytes LE)
sha256        32 bytes  (post-decompression checksum)
flags         u8        (bit 0 = compressed)

Data (0x02)

LT-encoded data symbol. Payload is exactly blockSize bytes. The seed in the header tells the receiver which subset of source blocks were XOR'd to produce this symbol. Same seed → same combination on both sides; the LT degree distribution makes belief propagation feasible.

Sender loop

  1. Compress source bytes (DEFLATE via pako) if size > threshold.
  2. Compute SHA-256 of the (zero-padded) compressed payload.
  3. Split into K blocks of blockSize.
  4. Build an LT (Luby Transform) encoder over those K blocks.
  5. Loop indefinitely: every 10th tick render a meta packet; otherwise pick a fresh random seed, encode an LT symbol, render the resulting bytes as a QR code.
  6. Stop on user cancel.

Receiver loop

  1. Open the camera, scan QR codes continuously.
  2. For each decoded frame: validate magic + version, lock onto the first fileId seen, discard frames with a different one.
  3. If meta and we haven't seen meta yet: store fileName, mime, size, sha256, flags. If data: feed (seed, payload) into the LT decoder.
  4. Track progress as recovered / K.
  5. When the decoder completes: reconstruct, verify SHA-256, decompress if the compressed flag is set, hand the file off to the OS (save / share intent).

QR rendering

Frames are rendered with error correction level L (~7% redundancy) — the fountain code already supplies massive end-to-end redundancy, so spending more QR bits on ECC just shrinks the per-frame payload for no gain. Quiet zone: 1 module. Sender pacing: ~12-13 frames per second (80ms tick); the receiver camera drops some, the fountain code recovers them.

What this protocol is not

Roadmap

Have an idea or spotted a flaw? protocol@transfqr.com.