← All posts

June 17 — making Loupe leave my box: portability, a brand, and a public site

This was a pivot day. Loupe has been a personal one-off — a photo culling tool wired to exactly my setup. Today I decided to aim it at other people who want to self-host it, and then spent the rest of the day paying down everything that assumption had let me get away with. Six threads, one throughline: make it portable, make it shareable, make it look like a product.

The decision that started it

I reframed Loupe as something other people could run, with a friend as test customer number one (Mac plus iCloud, his own storage). The strategic call: build features that generalize instead of ones hardcoded to me, and narrow the initial target to Mac + iCloud so I can lean into the Apple-photo-library extraction rather than rebuild that intelligence from scratch — it's the highest-value, hardest-to-replace layer. A fancier "setup console" idea got explicitly deferred: build it only after the pipeline is proven end-to-end for a second person, not before.

Built / shipped

A portability audit, then a portable spine. First a strictly read-only audit to find out how coupled the thing really was. The load-bearing finding: the enrichment database (aesthetic scores, people, scene labels) is 100% derived from a macOS photo library — there was no committed builder for it, just loose input files sitting around. That's the single biggest blocker for a Mac-less second user. So I:

A finished brand and a redesigned header. I locked the wordmark — the "o" is an isometric loupe lens on a glass stand sitting over a film strip, with the frame beneath the lens lit amber, like the loupe is examining the chosen frame. Then redrew the Overview header into a compact two-row band: logo left, breadcrumb stacked above a full-width stats line, buttons right.

A pre-deletion review surface. The cut and kept tallies in the stat strip are now clickable — each opens a filtered grid of everything currently pending-cut or pending-keep. That turns a passive counter into the place you eyeball the whole set before anything becomes irreversible, which is the whole ethos of this project. The counts read from the same state mirror the stats line uses, so the grid can never silently disagree with the number above it.

loupeculling.com. Every loupe.* domain was taken, so I ran a real availability sweep over a curated candidate list — I had to hit each registry's own lookup endpoint directly because the public aggregator rate-limited me into the ground. The "Loupe Culling" family was wide open. The marketing site is a separate static site, fully decoupled from the app, with faithful app mockups rebuilt in HTML from royalty-free stock photos only — never a real photo from anyone's library.

Problems & fixes

Publish-safe, the careful part

Making the repos public meant scrubbing them — and that's irreversible, so every step got a gate. I found secrets that had been committed in history, untracked them, and rewrote both repos' histories to purge those along with every runtime database, personal config, model weights, and cached photo previews. The first scrub pass missed the biggest blobs (the model weights and the preview cache), so the repo barely shrank until a second pass — always re-scan the largest objects after a history rewrite. The whole thing was gated on a full backup, a bundle, and a byte-identical restore of the live data with a row-count match. All passed. I also de-personalized the source — real names, home locations, owner identity all moved into gitignored config, with neutral placeholders shipping in the source.

Learned

Still open / next