my-library

Code map

A tour of the tree, and which parts run in production versus on a laptop.

my-library/
app/
  layout.tsx              Inter, metadata, footer
  globals.css             Tailwind 4 @theme tokens — the palette
  icon.svg                favicon; Next derives the rest
  page.tsx                the shelf
  books/[id]/page.tsx     one book
  components/
    BookCover.tsx         2:3 cover box + Rating
    SearchBar.tsx         the only client component
    Pagination.tsx        plain links, no JS
  api/v1/books/
    route.ts              GET list · POST one-or-many
    [id]/route.ts         GET · PUT · PATCH · DELETE

db/
  schema.ts               users, api_tokens, books
  client.ts               lazy proxy + test injection
  migrations/             generated SQL, committed

lib/
  auth.ts                 bearer token → user
  http.ts                 json() and the { error, code } shape
  validation.ts           zod: what a valid book is
  queries.ts              web reads: search, sort, page

scripts/                  local only — never deployed
  create-user.ts          mint a user + token
  create-token.ts         mint another token
  import-books.ts         OpenLibrary lookup + mapping
  enrich-library.ts       the big sharded, resumable run
  import-library-commit.ts  shards → database
  scan-local-books.ts     catalogue local ebook files
  ebook-meta.ts           MOBI/EXTH + PDF metadata parser
  export-catsby-drive.gs  Apps Script, runs in Google Drive

tests/
  books.test.ts           8 tests
  helpers/db.ts           pglite harness

postman/                  collection + environment (git-ignored)

Three tiers, and it matters which is which

RunsWhereWhat
On every requestVercelapp/, db/, lib/. Nothing here talks to OpenLibrary or the filesystem.
By hand, locallyA laptop, with .env.localscripts/. Long-running, network-heavy, and allowed to be slow because nobody is waiting on an HTTP response.
Once, elsewhereGoogle Driveexport-catsby-drive.gs — Apps Script, pasted into script.google.com. It is in the repo because it is source, not because it deploys.

Why the scripts are not API routes

Enriching the library is ~3,800 books at two OpenLibrary calls each, politely spaced. That is roughly an hour of wall time — far past any serverless timeout, and pointless to expose as an endpoint when it runs once. Keeping it in scripts/ also means the production bundle never carries the import code.

The files worth reading first

FileWhy
lib/validation.tsOne zod object defines a book. The API parses request bodies through it and the import parses OpenLibrary responses through it, so both reject the same things.
db/client.tsA lazy Proxy around Drizzle: importers get a handle, but no connection opens until first use, and tests can swap in pglite before that happens. See Database.
lib/queries.tsEverything the website knows about reading. Paging, ILIKE search, and the Bayesian rating order live here and nowhere else.
scripts/enrich-library.tsThe interesting one. Sharding, checkpointing and resume, because a one-hour job that cannot be interrupted is a job that never finishes.

Sibling repositories

The clients are separate repos, one per platform, each consuming /api/v1/books: my-library-ios, my-library-flutter, my-library-rn. They are covered in Mobile clients.