The Problem
Moving a file between two devices without cloud storage or a network connection is awkward. USB cables require drivers, Bluetooth pairing is slow, and cloud uploads expose data to a third party. QRFerry solves this with an air-gapped optical link: one screen displays an animated QR stream, a phone camera reads it, and the file transfers entirely in-browser.
What This Does
QRFerry is a Next.js app that encodes file data into a sequence of QR codes and decodes them on the receiving end. The sender runs at /, the receiver at /scan. The core protocol lives in lib/qr-transfer.ts (37 functions) and lib/optical-transfer.ts (10 functions); both handle framing, CRC-32 checksums, and RaptorQ fountain encoding for loss-tolerant reconstruction.
The pipeline compresses with Brotli/gzip (lib/compression.ts), renders QR codes via WebAssembly (lib/qr-renderer.ts), and scans with ZXing-C++ WebAssembly (lib/qr-scanner.ts). The receiver verifies four separate checksums before saving, and the file never touches a server.
How It Is Wired
Execution starts at app/page.tsx (sender) or app/scan/page.tsx (receiver). The sender path flows through app/send-client.tsx → lib/optical-transfer.ts → lib/compression.ts → lib/qr-renderer.ts. The receiver path goes app/scan/scanner-client.tsx → lib/qr-scanner.ts → lib/optical-transfer.ts → lib/compression.ts.
The highest-risk function is crc32 in lib/qr-transfer.ts, called from 11 places—change it and the entire integrity chain breaks. buildOpticalContainer and parseOpticalContainer in lib/optical-transfer.ts are the framing hub, each called from 2 files. The module graph shows no circular dependencies (0 of 29 modules), which keeps refactoring straightforward.
The only outbound network call is in worker/index.ts (a Cloudflare Worker), which handles auth redirects. The database layer (db/index.ts, db/schema.ts) uses Drizzle but is optional—the core transfer path is fully client-side.
How To Use It
npm install
npm run dev
Requires Node.js >=22.13.0. No environment variables are needed for the core transfer. npm test runs the codec, corruption-rejection, and end-to-end checksum tests. npm run lint and npx tsc --noEmit provide additional checks.
Real-World Use
A field engineer needs to transfer a 10 MB config file to an air-gapped industrial controller. They open / on a laptop, select the file, and point the controller's phone camera at the screen using /scan. The phone reconstructs the file after enough RaptorQ symbols arrive, verifies all checksums, and saves it—no USB stick, no network.
Code Health & Issues
Static analysis found 3 medium findings across 29 analyzed files:
- Medium - Deep nesting in
app/send-client.tsx: max indentation depth 6. Flatten with guard clauses. - Medium - Oversized files:
app/scan/scanner-client.tsxandlib/qr-transfer.ts(829 lines each). Split by responsibility.
SDLC gaps from the file structure:
- High - No LICENSE at repo root; default is all-rights-reserved, blocking reuse.
- High - No CI pipeline; 28 source files merge with no automated build/test gate.
- Medium - No Dependabot/Renovate; advisories sit unpatched.
- Low - Missing
.editorconfig,.gitattributes, formatter config; cross-contributor inconsistency.
Tests exist (tests/codec.test.ts, tests/rendered-html.test.mjs) and cover corruption, erasures, and real renderer-scanner round trips.
The Bottom Line
QRFerry is a technically solid implementation of a niche but real problem—air-gapped file transfer with strong integrity guarantees. The RaptorQ + multi-checksum design is sound, and the test harness is genuinely thorough. The main risks are maintenance ones: no CI, no license, and two files that need splitting. Use it if you need verified optical transfer; fix the license and CI before production.