Let me set the scene.
BoomerClip has exactly one promise: seen once, then gone. Record your screen, share a link, and the video disappears when the viewer is done.
A few posts ago I admitted the promise was held together with good manners. The recording got deleted when the viewer's browser politely mentioned it was leaving. The code review had found ways to fetch a recording, or keep one alive, that never involved the viewing page at all.
So Phase 2 had one job: make the server guarantee deletion, no matter what the browser does.
Check the changelog dates. The repo clean-up, the viewing-page fixes, the entire code review, all of Phase 1, and both halves of Phase 2 are stamped with the same day: 13 September. I don't know how. I've been told it happened. This is what I remember.
Part one: the front door was wide open
I started with uploads, feeling optimistic. That lasted about four findings.
The upload form could simply ask for a recording that never expires.
Read that again. The app whose entire identity is disappearing videos had a code path that let an upload mark itself as permanent. It came with its own storage branch that dutifully kept it forever. The self-destruct button had an off switch, and the off switch was reachable from the form.
It's gone. Every recording is ephemeral now, and the branch that honoured "please keep me forever" is deleted.
Then: the server trusted whatever file you handed it. Say it's a video? Sure, it's a video. Uploads are now probed in a folder the web server never serves, and kept only if they genuinely are WebM or MP4 with an actual video stream inside. The extension and content type come from what the probe found, not from what the upload claimed. Empty or broken files get rejected with a message, and a failed upload leaves nothing behind.
Error responses stopped handing out stack traces like party favours.
And the duration bug, the one from the very first post, the one I thought I'd fixed in 2025? Finally dead. Chrome's recordings have no duration header, so the length is now measured from the last packet in the file, instead of being stored as the string "N/A".
Tests went from 10 to 39. I felt great. I should not have felt great.
Part two: anyone could just take the video
Here is the finding that made me put my head on the desk.
The recording files were served from a public folder.
If you had the address, you could download the video directly. A direct download never joins a viewing session, so nothing would ever know you'd watched it, and nothing would ever delete it. All the careful confirm-before-refresh, grace-period, reconnect logic from earlier posts was guarding the front of a building with no back wall.
The public folders are gone. When the viewing page opens now, it joins a viewing session and receives a token. The video and its thumbnail are served only to a request carrying the token of a viewer who is present right now. No token, no video. The thumbnail is locked too, because a thumbnail is literally a frame of someone's screen.
Then came the question that ate an embarrassing amount of thinking: how long should that token live?
- Short and fixed? Very secure. It also breaks the single most normal thing a human does with a video: pause it, wander off, come back, and seek. Token expired, video errors, viewer confused, promise broken in a new and exciting way.
- Long? Playback never breaks. But a viewer who already left can keep downloading, as long as anyone else is still watching. Which is the exact thing we're trying to stop.
The answer: the token lives exactly as long as its viewer is present. It never dies under a paused player, and it dies the instant that viewer leaves. Not a second before, not a second after.
The server that forgot everyone
I thought the token was the hard part. The token was not the hard part.
Viewers were tracked in the server's memory. Lovely, right up until the server restarts. A restart forgot every single viewer. A recording being watched at that moment would then sit there until its 24-hour expiry, and its files would stay on disk forever. A crash doesn't send a goodbye. A crash just leaves.
I went through three designs. Two of them tried to hurt me.
- Copy the viewer list into Redis, and on startup start deletion timers for everyone found. Simple. Also, if two server processes ever share Redis, each one starts timers for the other's live viewers and deletes recordings people are actively watching. Beautiful. Rejected.
- Have Redis announce when a viewer's key expires. Those announcements are fire-and-forget. If no server happens to be listening at that moment, the event evaporates. A deletion guarantee built on "hopefully someone heard" is not a guarantee. Rejected.
- Leases. Every viewer gets a "present until" time 30 seconds ahead. The server holding their connection renews it every 10 seconds. A regular check deletes any viewed recording that nobody is present for.
Leases won, because they need nobody to report the failure. That's the whole trick. A crashed server can't tell anyone its viewers are gone, and it doesn't have to. The leases just run out, and whichever server is alive next does the deleting. A viewer whose Wi-Fi blinks still gets their 15-second grace. A viewer who reconnects presents the same token and carries on watching.
And a graceful shutdown, like restarting for an update, stops tracking before the connections drop. Otherwise every restart would look like every viewer storming out at once.
Cleaning up the bodies
Deletion itself turned out to have been in the wrong order.
The record pointing at a recording could be removed first, and if deleting the file then failed, you had a video on disk that nothing referred to. An orphan. Unfindable, undeletable, eternal. Now files go first, and a failure retries instead of shrugging and moving on.
For anything that still slips through, there's a storage sweeper. It runs at startup and every ten minutes, removing recording files whose record has vanished and abandoned uploads, once they're 15 minutes old.
(Remember that sweeper. It matters later.)
Meanwhile, in the smaller-fires department:
- The view counter could bring deleted recordings back from the dead. It would recreate the record with no expiry. It also counted every range request as a view, and a video loads in many range requests. It's now atomic, can't resurrect anything, and counts once per viewing page.
- A single read error while streaming could crash the entire server process. Streaming now runs on Express's built-in
sendFile, and about 270 lines of per-browser special cases went in the bin with great satisfaction. - Any signed-in account could delete any recording. Now every upload returns a creator token, once. The server keeps only a hash of it, so even reading the database doesn't give you the power to delete. A signed-in uploader's account works too, and admins can delete anything, because deleting only ever adds privacy.
- The rate limits didn't know about the proxy. Deployed behind one, they'd have seen the proxy's address instead of each visitor's, and throttled everyone as if they were one person. They now use each visitor's real address, with separate limits for sign-ins, registration and uploads.
The sweeper strikes
Here's where I nearly lost it.
I don't trust a test until I've watched it fail. So every fix in this phase got broken on purpose, to prove its test would catch the break. Twenty-nine deliberate sabotages.
One of them switched off the sweeper's safety check: the part that confirms a file really has no record before deleting it.
I made that edit in the real source file.
While the development server was running.
Configured to restart whenever a file changes.
It restarted. On my sabotaged sweeper. Which immediately ran its startup pass. Which deleted the video and thumbnail of one of my real local recordings.
I didn't even see it happen. I found it later, because a recording's record was still there and its files weren't. So I did what any calm, well-adjusted developer does: I stared at modification times.
The storage folder was last changed at 10:09:44.
The sabotage went in at 10:09:45.
One second. The files were gone, and there was no getting them back.
~ Jer