SWE Case Study
Refract
A privacy-first media library: the server never stores media bytes. Three-tier metadata storage with HEIC progressive loading via Web Worker.
Snapshot
At a Glance
Role
Founder & Engineer
Sole developer
Duration
6 mo
Oct 2025 – Mar 2026
Capacity
200K items
POWER tier metadata limit
Storage
0 bytes
Server stores zero media — only metadata
01 — Context
The Constraint
Refract is a media-library product built over roughly six months (October 2025 – March 2026) and pre-launch as of authoring. Photo collections on consumer cloud drives — Google Drive, Dropbox — accumulate without organization. Existing AI-tagging products solve that, but they require shipping every byte of every photo to a third-party server: a DMCA surface, a breach surface, and a storage cost passed back to the user.
The load-bearing decision sits one level above the feature set: the server never stores media bytes. Only metadata — file IDs, names, MIME types, thumbnail URLs already issued by the user's drive provider. Original media stays on the drive provider or local device. That single boundary eliminates whole classes of legal, operational, and security risk before any feature is built on top of it.
Everything else in the system — the storage layer, the AI tagging pipeline, the auth surface — is downstream of that constraint. Zero server-side media bytes is not a marketing posture; it is the engineering envelope every other component had to fit inside.
02 — What I Built
System Shape
The frontend is a React 19 + Vite 6 single-page app — explicitly not Next.js, a decision worth flagging now and revisiting in Section 04. TypeScript 5.8 strict, ES2022 target. The backend is a set of Vercel serverless functions under /api. Authentication is Google OAuth 2.0 only; both Drive and Dropbox data flow downstream of a single Google identity, which keeps the consent surface to one provider.
The metadata layer is three tiers. PostgreSQL via Prisma Accelerate (Prisma 7.4) is the canonical relational store — schemas, user records, tier quotas, the slow-but-correct source of truth. Vercel KV holds a hot index per user (~200 bytes/user) so the most-recent-library query returns from cache in one round trip. Vercel Blob holds gzip-compressed JSON chunks of one thousand items each — the actual library payload, partitioned for bandwidth rather than queried with SQL. KV points at Blob; Blob points at the user's drive provider; nothing in the chain holds a media byte.
The AI layer runs in the browser. The @google/genai SDK (1.30) calls gemini-2.5-flash for image analysis and gemini-2.0-flash for tag generation, both from client-side. The user's Gemini quota, the user's machine, the user's photos — server stays uninvolved. UploadThing 7 handles the asset boundary for direct file uploads (when the user opts into local-file workflows), with tier quotas of 10/250/1000 items for FREE/PRO/POWER. Stripe handles the subscription side. Sentry instruments both the SPA and the serverless functions.
Other engineering on Refract
03 — Hard Parts
What Was Hard
Three-tier metadata storage (ADR-2)
Library metadata can grow to two hundred thousand items at the POWER tier. A single Postgres query scanning that shape does not return in human-acceptable time once a user is past the first few thousand photos; a Redis-only cache wouldn't survive cold reads on a user who'd been inactive for a month.
Settled on three tiers: Vercel KV as a hot index keyed by user (about 200 bytes per user, holds pointers and an LRU head); Vercel Blob as cold storage of gzip-compressed JSON chunks at one thousand items per chunk; /tmp as a dev fallback so the local loop doesn't require provisioned infra. A registry pattern on top of KV gives O(1) file-ID lookups without loading any chunk.
The chunk granularity (1,000) is a guess. Too small means more round trips per library scroll; too big means wasted bandwidth on an item the user never sees. Settled empirically against the test fixture — past that, optimizing further would need user-data telemetry, and the privacy envelope means there isn't much of that to draw on. Open tradeoff.
HEIC progressive loading via Web Worker (ADR-6)
HEIC is the default photo format on iOS. Browsers don't decode it natively. Conversion is CPU-heavy enough to freeze a tab if you do it on the main thread for a bulk library scroll.
Built progressive load: the thumbnail Drive already issues shows instantly; a background Web Worker (heicWorker.ts) runs heic2any against the original; the full-resolution JPEG replaces the thumbnail when ready. A worker pool throttles concurrency so a thousand-photo bulk load doesn't spawn a thousand workers and lock the tab.
Tradeoff is explicit: heic2any ships as roughly 150kB extra in the bundle, paid by every user — including Android users who'll never encounter a HEIC file. iOS is a known target audience for a media-library product, so the bytes are accepted. A conditional dynamic import was considered; the cost there moves to first-HEIC-encounter latency for the user who actually has them, which was the worse failure mode.
04 — What I'd Change
In Hindsight
The Vite/Vercel split feels off-pattern. The rest of the suite from the same product line ships on Next.js, and Refract started as a Vite SPA back when Next.js's static-SPA story was rough — the build pipelines didn't want to cooperate for a heavily client-side app. With Next 15 in hand, would build it as a Next.js app from the start: shared eject and deploy patterns across the portfolio, a unified RSC + streaming option for any future server-touching surface, one dependency tree to keep current instead of two.
The cost of the current shape is paid every time a cross-product utility is written — auth helpers, Stripe webhook plumbing, Sentry config — each one has to support two build pipelines. Not expensive per utility; expensive in aggregate, and the tax compounds the longer the codebase lives.
View Product
Built at Ramiro Labs.