Shelfie -- Architecture
Internal reference. Not exposed in the app. Updated by hand when the stack changes.
Flow diagram
┌──────────────────┐
│ OpenLibrary │
│ search.json │
│ (no auth, free) │
└────────▲─────────┘
│ enrich:
│ title/author/subjects/year/ISBN/cover
│
┌──────────────────┐ HTTPS ┌────────────────────────────────┐
│ Browser / PWA │────────────▶│ Vercel Edge / Functions │
│ shelfiebook.com │◀────────────│ Next.js 15 App Router │
│ iOS+Android+Web │ │ middleware.ts (auth gate) │
└────────┬─────────┘ │ │
│ │ Server components: │
│ camera capture │ /library/* /scan/* /u/* │
│ multipart upload │ │
▼ │ Server actions / API routes: │
┌────────────────┐ │ /api/spines (vision) │
│ Spine photo │──────────────▶│ /api/identify-cover │
└────────────────┘ │ /api/enrich /api/import │
│ /api/export /api/recommend │
│ /api/books /api/v1/books │
│ /api/migrate-local │
│ /api/library-profile │
│ /api/api-keys │
│ /api/stripe/{checkout,webhook,│
│ portal} │
│ /auth/callback │
└─┬────────┬──────────┬──────────┘
│ │ │
│ Bearer │ Auth │ JWT
│ JWT │ cookies │
▼ ▼ ▼
┌───────────────────────────────────────┐
│ Supabase │
│ ┌─────────────┐ ┌────────────────┐ │
│ │ Postgres │ │ Auth (GoTrue) │ │
│ │ (with RLS) │ │ magic links │ │
│ └──────┬──────┘ └────────┬───────┘ │
│ │ │ │
│ profiles, libraries, │ │
│ books, photos, │ │
│ book_photos, │ │
│ shelf_labels, │ │
│ smart_shelves, api_keys, │ │
│ embeddings (pgvector) │ │
│ │ │
└────────────┬───────────────┘ │
│ │
┌────────────┘ │
│ SMTP relay │
▼ │
┌──────────────────┐ │
│ Resend │ auth@shelfiebook.com │
│ smtp.resend.com │ delivers magic-link emails │
└──────────────────┘ │
│
┌───────────────────────────────────────────────────┘
│ direct (server-side only)
▼
┌────────────────────────┐ ┌────────────────────────┐
│ Anthropic API │ │ Google Gemini API │
│ claude-sonnet-4-6 │ │ gemini-2.5-flash-image│
│ - readSpines │ │ one-shot UX graphics │
│ - readCover │ │ (npm run gen-graphics)│
│ - recommendForCluster │ └────────────────────────┘
│ - library-profile │
│ prompt caching ON │
└────────────────────────┘
┌────────────────────────┐ ┌────────────────────────┐
│ Voyage AI │ │ Google Books API │
│ voyage-3 embeddings │ │ fallback enrichment │
│ (planned: pgvector) │ │ if OpenLibrary misses │
└────────────────────────┘ └────────────────────────┘
┌────────────────────────┐ ┌────────────────────────┐
│ Stripe │ │ Cloudflare R2 │
│ Checkout + Customer │ │ shelfie-photos bucket │
│ Portal + Webhooks │ │ (planned for cloud │
│ freemium tiers │ │ photo storage) │
└────────────────────────┘ └────────────────────────┘
┌────────────────────────┐
│ GitHub │ source of truth: ERoske/shelfie (private)
│ ERoske/shelfie │ Vercel deploys via CLI; auto-deploy planned
└────────────────────────┘
Local mode (no cloud)
When SHELFIE_DB_MODE=local (the default in .env.local), the data plane shrinks to just the browser + filesystem:
Browser ──▶ Next.js (localhost:3003) ──▶ fixtures/library.json
│ fixtures/spines/*.json
└──── localStorage (overrides, status, rating, notes, etc.)
Cloud-only services (Supabase, Resend, Stripe) are skipped. Anthropic + Gemini still work because they're stateless API calls.
Tech stack table
| Layer | Tech | Purpose | Where |
|---|---|---|---|
| Hosting | Vercel | Next.js production runtime, SSL, edge cache, atomic deploys | shelfiebook.com |
| Domain registrar | Name.com | DNS for shelfiebook.com | A record + CNAME pointing to Vercel |
| Framework | Next.js 15 (App Router) | UI, server components, API routes, middleware | top-level repo |
| Language | TypeScript 5.6 | All app code | tsconfig.json |
| Styling | Tailwind CSS 3.4 | All component classes | tailwind.config.ts |
| Type system | React 19 + @types/react 19 | Type checking against React 19 APIs | n/a |
| Auth | Supabase Auth (GoTrue) | Magic-link sign-in + sessions | middleware.ts, lib/supabase/* |
| Database | Supabase Postgres | All user data, with Row-Level Security | supabase/migrations/* |
| Email relay | Resend | Magic-link transactional email | smtp.resend.com (configured in Supabase) |
| Vision OCR | Anthropic Claude Sonnet 4.6 | Read book spines from photos | lib/claude.ts (readSpines, readCover) |
| Recommendations | Anthropic Claude Sonnet 4.6 | Gaps + library-profile generation | lib/claude.ts (recommendForCluster) |
| Metadata | OpenLibrary REST | Enrich book metadata (free, no auth) | lib/enrich.ts |
| Metadata fallback | Google Books v1 | When OpenLibrary misses | lib/enrich.ts |
| Embeddings (planned) | Voyage AI voyage-3 | Semantic "Books like this" | lib/embeddings.ts |
| One-shot graphics | Google Gemini 2.5 Flash Image | Generate static UX assets (hero, empty states, logo) | scripts/gen-graphics.ts -> public/graphics/ |
| Payments | Stripe | Subscriptions for Plus + Pro tiers | lib/stripe.ts, /api/stripe/* |
| Photo storage (planned) | Cloudflare R2 | Cheap S3-compatible blob store | not yet wired |
| Vector search (planned) | pgvector (Supabase extension) | "Books like this" cosine search | enabled in 0001_init.sql |
| Graph render | Cosmograph WebGL | Library-as-graph view | components/LibraryGraph.tsx |
| Spreadsheet export | xlsx (npm) | Build .xlsx exports | app/api/export/route.ts |
| Postgres driver | pg + pgvector | Migration runner | scripts/migrate.ts |
| MCP server | @modelcontextprotocol/sdk | Expose library to Claude Desktop / Cursor | mcp/bin.js |
| CLI | (vanilla node fetch) | Command-line wrapper over /api/v1 | cli/bin.js |
| Source control | Git + GitHub (ERoske/shelfie, private) | Code history | n/a |
| CI / deploys | Vercel CLI (vercel --prod) |
Push to production | scripts/push-env.ts, scripts/add-domain.ts |
Where every secret lives
| Secret | Used by | Stored in |
|---|---|---|
ANTHROPIC_API_KEY |
server: lib/claude.ts | .env.local + Vercel env (3 envs) |
GOOGLE_API_KEY (Gemini + Books) |
server: scripts/gen-graphics.ts, lib/enrich.ts | .env.local |
SUPABASE_*, NEXT_PUBLIC_SUPABASE_* |
server + client | .env.local + Vercel env |
SUPABASE_SERVICE_ROLE |
server admin only (bypasses RLS) | .env.local + Vercel env |
SUPABASE_DB_URL |
scripts/migrate.ts | .env.local (not in Vercel) |
RESEND_API_KEY |
server: app uses Resend SDK; Supabase SMTP password | .env.local + Vercel env + Supabase Auth SMTP form |
STRIPE_SECRET_KEY |
server: lib/stripe.ts | (planned) Vercel env |
STRIPE_WEBHOOK_SECRET |
server: /api/stripe/webhook | (planned) Vercel env |
STRIPE_PRICE_PLUS, STRIPE_PRICE_PRO |
server: lib/stripe.ts | (planned) Vercel env |
Service-role / DB password are never exposed to the browser. Anything NEXT_PUBLIC_* is.
Data flow: a typical shelfie
- User opens
/scanon iOS, taps the camera button. - Browser uploads JPEG to
/api/spinesvia multipart form. - Vercel function downsamples, sends image to Anthropic Claude Sonnet 4.6 with cached system prompt +
emit_spinestool. - Response is
[{title, author, confidence, bbox}]. Function stashes it in sessionStorage and redirects to/scan/review/<live_id>. - User accepts / fixes spines. Two-tap fix opens
/api/suggest(OpenLibrary search) for candidates. - On Save, each spine becomes a row via
/api/v1/books(cloud) or the localStorage additions store (local). - Cloud writes go to
books(RLS-gated bylibrary_id); the samebookKey()normalization keeps duplicates from any future re-shelfie collapsing into one row. - Library-page server component re-renders with the new books, photos, and book_photos rows.