Replay
A local-first timeline that turns a camera roll back into trips and days.

- Year
- 2025
- Role
- Design and engineering
- Stack
- TypeScript · Electron · SQLite
The problem
A camera roll is chronological and nothing else. The structure you actually remember (the trip, the day, the afternoon something happened) exists in your head and nowhere in the file system.
The approach
Replay reads a photo and video library on Windows and reconstructs that structure locally. Nothing is uploaded; the library never leaves the machine.
- Clusters captures into trips, days and moments from time and location.
- Local-first by design: no account, no sync, no cloud dependency.
What was hard
Clustering is easy to make work on your own library and hard to make work on someone else's. Threshold-tuning that felt correct on a dense London camera roll fell apart on a sparse one.
What I'd change
The database call is synchronous, and that is not a theoretical problem.
node:sqlite's DatabaseSync blocks the whole event loop for every other
request while a query runs. Before I added the pairing index, the Live Photo
subquery took 13.2 seconds against 2,370 assets, while the client polled it
every 2.5 seconds. Splitting the reads and adding that index took the library
endpoint to about 27ms, and a timeline page to 14–79ms.
Two smaller holes of the same shape. Updating an asset is a plain read-then-write with no version check and no transaction, so two overlapping edits silently lose one. And the recursive file walk has no error handling and no depth limit, while the source-root check only requires a path of three characters. Register something too broad and a single permission-denied subdirectory throws away the entire scan with no partial progress kept.
The change that matters is neither of those. It is the acceptance suite the roadmap already lists and I never built: automated proof that originals stay byte-identical after a scan, that a rescan is idempotent, and that a disconnected root cannot prune records. The whole pitch of this tool is "chronological truth, originals never touched", and that currently rests on my having been careful rather than on anything that fails a build when it stops being true. Three test files exist. The stated design target of 10,000 records under 250ms has never been verified, and the README says so.
What I would reverse is the first attempt at derivation: two environment flags gating eager thumbnailing, which the dev script never set. The indexer worked perfectly and the app showed almost nothing (2,076 of 2,375 assets had no preview) for as long as that went unnoticed. Derivation is now a second job type competing in the same poll loop, prioritised over scans in SQL.