Flit Devlog #5
Last time I ended on “it’s live, it’s public, anyone can hit the URL.” That felt great for about a day, until I thought about what “anyone can hit the URL” actually means. A public drop box where every visitor can read every drop isn’t a feature, it’s a liability. So this entry is me getting paranoid and cleaning up after myself.
The public box problem
I wanted encryption without a server that holds keys. If the box is public, the server should store gibberish. So it happens in the browser: type a passphrase and everything runs through PBKDF2 (150k rounds, SHA-256) into an AES-256-GCM key before it leaves the tab. The server only ever sees a FLITENC1: blob.
Great, until I tested over the LAN and got TypeError: Cannot read properties of undefined (reading 'deriveKey'). crypto.subtle wasn’t there. Web Crypto only exists in a secure context, so localhost and HTTPS work, but http://192.168.x.x on another device is undefined. So I documented it (HTTPS or a VPN hostname for off-localhost encryption) instead of pretending.
A status light that lied
The “Live” dot is driven by the SSE onopen. Locally it fired instantly; on the deployed box it sat on “Connecting…” while drops streamed in fine. The proxy was buffering the first chunk, so onopen fired late. Fix: send one byte on connect (a ready event prepended to the stream) so the proxy flushes and the client knows it’s open.
A server that wouldn’t die
Added graceful shutdown; Ctrl+C then did nothing. with_graceful_shutdown waits for every connection to close, and the SSE stream never closes, that’s its whole point. So it waited forever. I stopped being clever: print, spawn a 500ms timer, std::process::exit(0). Not elegant, but it actually turns off.
Handing over one thing, not the keys
Two mirrored features. Share links (/s/{id}) hand someone a single item, optionally one-time or expiring, without exposing the inbox. Drop links (/d/{id}) are post-only: a guest can send something into my box but can’t see what’s inside.
Making it a real app
A manifest and service worker so Flit installs to the home screen and loads offline. Web Share Target so Android’s “share” can target it. A server-rendered SVG QR (qrcode, default-features = false) so pairing a phone is a scan, not a typed IP.
And then it all broke on camera
Not proud of this. I’d rewritten the whole front-end in one sitting, shipped it, hit record for the demo, and watched it faceplant.
Clicked “Share”: dead button, no request. Console: Uncaught ReferenceError: POST is not defined. I’d written method: POST instead of "POST" — a bare identifier that threw before fetch.
The drop link “worked”: it copied the string undefined. The culprit was await r.json without the (), so j.url read off the function, not the body. One () fixed it.
Once I actually read the file, the rest tumbled out: uploads sent fd.append("file", blob.name) (the filename as text, not the file), download was a.href - URL.createObjectURL(...) (a minus sign, evaluates to NaN), files never rendered because the check was it.kind === "kind", and the status indicator pointed at a #status element that no longer existed.
The infuriating part: the Rust backend was innocent. I traced the whole request path expecting a bug in create_drop or the router and found nothing wrong. The crime scene was entirely the front-end, a pile of one-character typos I’d never have caught without reading it line by line. “It compiles / it loads” is not “it works,” and the front-end has no compiler to yell at you. You find out on camera.
Where Flit stands
- Real-time public/private drop box: text, links, files
- Browser-side E2E encryption
- Share links and post-only guest drops
- Installable PWA: offline shell, share target, QR pairing
- Dark mode, EN/KO, rate limiting, ephemeral mode
- One binary, Docker image, on Render
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.