From Photography to AI

Seven months of a static page, then a database, a storage adapter, and a pipeline

Photography AI·Advanced·7 min read · July 9, 2026

The Photograph

There's a photo in this gallery tagged "street" — one of the ones the search pipeline would later use to prove a ranking bug. Back in November 2025, when I added the photography page, that tag didn't exist. Nothing did. The page was a component with a title and a static image or two, reachable from the header nav, and that was the entire feature.

The Problem

Nothing was technically wrong with it. It rendered. It was a section like any other on the site. The problem was that it couldn't become anything else. There was no database row behind a photo, no way to add metadata, no way to ask it a question. If I wanted to search my own photos by what was actually in them, this page had no path to get there — it wasn't half-built toward that goal, it simply wasn't pointed at it.

For seven months, that was fine. In February 2026 I refactored Photography.tsx alongside About, Work, Education, and Skills into a shared Section component — a UI consolidation, not a capability change. The photography page got a nicer wrapper and nothing more.

The Investigation

What changed wasn't a plan to build "an AI photo feature." It was that the rest of the platform had already answered a harder version of this question twice — cover letters and CVs needed deterministic evidence scoring before Claude wrote anything; Ask Vikram needed a retrieval layer before it could answer a question about an engineering decision. Both of those systems started from a database table and a query layer, not from a page.

Photography didn't have that. A photo lived nowhere except a filename and whatever I remembered about it. There was no way to even ask "show me the ones tagged street" because "tagged" wasn't a concept that existed yet. The gap wasn't AI — it was structural, and it was the same gap every other subsystem on this platform had already closed before touching a model.

The Decision

I could have bolted a caption generator onto the existing static component — hand a photo to Claude, render whatever came back, ship it in an afternoon. I didn't, for the same reason the cover-letter pipeline doesn't let Claude decide what's true: a caption with nowhere to persist to, no schema, no query layer, is a demo, not a feature. It would have looked done and been unextendable the moment I wanted tags, or search, or a second language.

So on 2026-06-23, the actual first commit of this system wasn't an AI call at all — it was a data model: a photo table, a Firebase Storage adapter reusing the same getFirebaseAccessToken() helper the rest of the platform already had, and a query module. Admin upload with sharp (image processing) and EXIF extraction came two days later. Nothing about AI enrichment existed yet. The foundation had to be boring and correct before anything on top of it was allowed to be interesting.

The Architecture

Static Photography.tsx (Nov 2025 – Jun 2026) ↓ photo / photo_exif / photo_tag tables (2026-06-23) ↓ Firebase Storage adapter (GCS REST, reused getFirebaseAccessToken()) ↓ Admin upload — sharp (resize/thumbnail/EXIF-strip) + EXIF extraction ↓ status = "draft" → ready for AI enrichment (next chapter)

Every later chapter in this series — Claude Vision, embeddings, hybrid search, the grounded assistant — sits on top of this one decision: photos are database rows with a lifecycle, not files with captions.

Lessons Learned

I didn't expect the gap between "static page" and "AI-ready system" to be entirely non-AI work. Looking back at the commit history, the first AI call in this entire feature doesn't happen until three days after the first commit. I'd have guessed the ratio was closer to even; it wasn't close.

What I'd do differently: the seven-month gap wasn't a mistake, but I didn't recognize until writing this retrospective that the pattern — deterministic substrate first, model second — wasn't a photography-specific insight. It was already the platform's rule by the time I got here. I re-derived it instead of just applying it on day one.

Looking Forward

With status = "draft" and a real photo sitting in Firebase Storage, the next problem wasn't search — it was much narrower: what does it mean to let a model look at one specific photo and describe it, when the photo itself is the one input in the whole system I don't fully control?