imn@dev:~/oss/spidey-shelf$ cat readme.md

gh://spidey-shelf

OSS

collection tracker · funko pops, spider-man only

one question, asked from a shop floor: does ilya already own this pop. a friend types the number printed on the box and gets a stamped verdict — OWNED, NOT OWNED YET, or NOT OWNED (was in the collection once). no account, no message to send, no app.

under the pixel-handheld skin sits a real data problem. there is no public funko api, so the 247-row catalog is hand-curated and source-linked rather than scraped, and pop numbers are not unique across variants — slug is the natural key, and both search and the scanner return every sibling sharing a number and make the owner confirm which one.

every external integration is optional, keys-off-by-default and cached in postgres. one nightly cron is the only thing allowed to spend a metered call, capped at roughly 38 ebay requests a day against a 5,000 ceiling, so a public page view can never cost anything. a new city is geocoded once, at write time, so the map never dials openstreetmap either.

map of acquisition cities: amsterdam, munich, moscow, haifa, batumi, tbilisi

imn@dev:~/oss/spidey-shelf$ cat scheme.md

friend's phone

types the number on the box

search / scan

next.js on vercel

fts + pg_trgm · server components

drizzle

catalog ×247

reference_figures

owned shelf

verdict view

price cache

nightly cron

only the 6:00 cron may spend a metered api call — pages just read

// deploy: Vercel (fra1) · Railway Postgres · one nightly cron at 06:00 UTC

imn@dev:~/oss/spidey-shelf$ cat services.md

Vercel
hosts the next.js app, the image cdn, the admin server actions, and the one 06:00 cron that refreshes prices.
Railway Postgres
the single database — catalog, collection, price cache, plus a hand-written tsvector and pg_trgm search layer drizzle-kit cannot generate.
UploadThing
stores owner-uploaded box art, normalized to 800x800 webp in the browser; its cdn host is pinned exactly, never wildcarded.
UPCitemdb
keyless 100-a-day barcode fallback for a scan the catalog misses — exactly one attempt, 5s timeout, no retries.
eBay Browse API
optional market-signal pricing — median, min, listing count — cached in postgres; without credentials the panel renders and calls nothing.
Nominatim
geocodes a newly seen acquisition city exactly once, inside the server action that saves the sighting, never at render time.
GitHub Actions
lint, prettier, typecheck, vitest and a full next build on every pr — with no DATABASE_URL, which is why db routes are force-dynamic.

imn@dev:~/oss/spidey-shelf$ cat stack.md

Next.js 16React 19TypeScript strictTailwind v4Drizzle ORMPostgresjose + bcryptjszxing-wasmVitest

imn@dev:~/oss/spidey-shelf$ cat highlights.md

SEARCH
digits take an exact pop-number match across the whole catalog, not just the owned half; text goes through websearch_to_tsquery OR'd with pg_trgm similarity. owned rows always sort first, so a 60-row limit can never hide the gift answer.
SCAN
the catalog shipped with zero barcodes, so a scan is an enrichment write rather than a lookup: miss → one upcitemdb call → a fuzzy match against the catalog → the owner confirms → the code lands on that row. the second scan of the same box is free.
BUDGET
external quotas are treated as load-bearing constraints. upcitemdb gets one attempt behind a 5s abort signal, and all bulk pricing is routed through a single nightly cron so every visitor-facing page only ever reads the cache.
AUTH
src/proxy.ts does an optimistic cookie redirect and says outright in its own comments that it is not auth, citing CVE-2025-29927. requireAdmin() re-verifies the jose-signed session inside every admin page, server action and route handler.
MAP
the sightings map is 27kb of vendored natural earth land data drawn through a hand-written equirectangular projection with antimeridian splitting — no map library, no tile server, no runtime dependency at all.
TESTS
around 60 vitest files run with zero database access: pure logic (slugging, search parsing, verdicts, stats math, upc check digits, map geometry) plus a mocked-db seam for the server actions.