The constraint
Three constraints pinned the design before I wrote a line of code:
- No re-uploads. 6,000 photos at ~5 MB each is 30 GB. The photos already live in Drive — copying them anywhere else would be the most expensive step in the whole flow.
- Resumable. Family members were going to do this in 15-minute chunks across days. Losing progress on a refresh was unacceptable.
- Phone-first. Everyone was going to use it on a phone, sitting on a couch. Tinder's swipe UX is the right primitive — keep, discard, or "yes please" — because it's literally what people are already doing in their heads.
What I built
- FastAPI backend with two surfaces: a
/photosendpoint that streams the next photo straight from Google Drive (no local copy), and a/verdictendpoint that records keep/discard/favorite into SQLite. - Google Drive API with a service-account credential — the backend opens an authenticated stream to the file and pipes it to the client. Drive does the bandwidth, my server does the routing. Bytes never hit my disk.
- Vanilla JS on the frontend — three swipe gestures (left = discard, right = keep, up = favorite), preloads the next two images so swipes feel instant, persists the cursor in
localStorageso refreshing doesn't lose your place. - SQLite verdict log, one row per photo per user — supports multiple family members reviewing in parallel without conflicts.
- Tailscale exposed it inside the family network on port
:8002. No public hosting, no ad-blockers, no auth grief.
Architecture highlights
What I learned
The right answer to "I have 30 GB of media and a UX problem" is almost never "build storage." It's "stream the bytes you already have and put a tiny database next to the verdicts." This is the same lesson that shows up everywhere applied AI lives: the model isn't the bottleneck, the data plumbing is.
The other lesson: weekend tools should look like weekend tools, not like products. I spent zero minutes on a logo, no time on settings, no time on accounts. The whole thing fit in three files. It worked the first time someone tried it because there was nothing in the way.
The point of a portfolio isn't only the load-bearing flagship projects. It's also the "I had a problem on Saturday morning and shipped a tool by Sunday night" projects, because that's the actual signal of how someone works.
Stack
Python · FastAPI · Google Drive API · SQLite · Vanilla JS · Tailscale