Reference

Decisions

The architecture decision records, and where to find the full text.

Non-obvious, reversible-but-significant decisions are recorded as ADRs in docs/adr/.

Add a new numbered record when you change the wire format, the fountain PRNG or degree table, the isolate/threading model, disk persistence semantics, or a language or runtime choice.

#DecisionStatus
0001Flutter receiver worker isolateAccepted
0002Fountain (LT-code) vs. sequential chunk encodingAccepted
0003Disk hydration designAccepted
0004Sender language — TypeScript → RustAccepted (QR-display sender only)
0005mobile_scanner stays pinned at the vendored fork (7.2.0)Accepted (known gap)
0006porter join ported to Rust; nodejs/ and all JS deletedAccepted (resolves 0004's defer)

In brief

0001 — Worker isolate

Decoding ran on the UI isolate and froze the app during a 109 MB transfer. A per-call compute() was rejected because the decode state must persist across thousands of calls; one long-lived worker owns it instead.

0002 — Fountain coding

A QR slideshow has no back-channel. Sequential encoding stalls on a consistently-missed frame; fountain removes the positional dependency at the cost of per-symbol decode overhead. Both are implemented and sender-selectable — the leading F token disambiguates.

0003 — Disk hydration

Chunk filenames are the ground truth for what has been received, because debounced metadata.json writes can lag the actual files. Hydration records what exists without reading it; an earlier eager implementation exhausted memory on real transfers.

0004 — Rust sender

"Install Node to send a file" is friction an air-gapped tool should not impose. Rust gives a single static binary and ratatui/crossterm for the interactive controls. Go was considered and lost on the TUI ecosystem.

0005 — Pinned scanner fork

mobile_scanner is vendored at 7.2.0 with a local patch adding macOS external-camera selection. Upstream is at 7.4.0; re-diffing risks the feature the fork exists for, for mostly-irrelevant fixes. Recorded as a gap.

0006 — Porting join

Joining is the last step of an air-gapped transfer, so it runs on exactly the class of machine that motivated ADR-0004. join is pure local file concatenation plus a SHA-256 check — no networking, no rendering, no wire-format surface — so porting it allowed nodejs/ to be deleted entirely.

One deliberate behavioural difference: stray files sharing a .part prefix are skipped rather than concatenated in.

Copyright © 2026