Skip to content
← All work

Replay

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

Replay: "The shape of your time.", with a year scrubber marking the months a trip falls in
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.

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.

Zain Butt · London, UKEmailCVGitHubLinkedIn