imn@dev:~/oss/wishka$ cat readme.md

gh://wishka

OSS

wishlist · surprise mode by schema design

wishka replaces the shared notes-app wishlist. the complaints in the readme are concrete — duplicated gifts, no images, mixed currencies, no way to hint sizes — and every feature maps back to one of them rather than to a generic feature list. paste a link, get a card, share the list, let people reserve without spoiling the surprise.

adding a wish carries most of the engineering. a pasted url runs a layered parser: open graph and json-ld first, then an llm pass over cleaned html if fields are still missing, then jina reader if the plain fetch was blocked, then firecrawl as a last resort — each layer only runs on a real gap. every fetch clears a hand-written ssrf guard first, and any image found is re-hosted instead of hotlinked.

the reservation system rests on one invariant: the wishes table carries no reservation state at all. "taken" lives only in a separate table, so an owner-facing query cannot leak a reservation even through a bug — there is no column to select. ai stays small and explicit: three quota-gated buttons, never a background process.

a wish card assembling from parsed fields: title, price, description

imn@dev:~/oss/wishka$ cat scheme.md

pasted product url

ssrf guard

L0 og / json-ld

L1 llm extract

L2 jina reader

L3 firecrawl

cache once · re-host image

wish card

reserve without spoiling

reservations live in their own table — the owner's query has no column to leak

// deploy: Vercel · Railway Postgres · nightly age-encrypted backup

imn@dev:~/oss/wishka$ cat services.md

Railway Postgres
the primary database — wishes, reservations, groups, profiles, auth tables, the per-url parse cache and per-day ai usage counters.
OpenAI API
html and markdown extraction inside the parsing pipeline plus generated wish images; model names are environment variables, not constants.
UploadThing
image storage and cdn — every parsed or generated image is re-hosted here, so nothing is ever served from a third-party host.
Resend
transactional mail — sign-in codes and reservation notifications, rendered through one shared paper-ledger template.
Jina + Firecrawl
parsing layers L2 and L3 — the free fallback when an anti-bot challenge blocks the plain fetch, then a paid last resort.
Vercel
hosting, and the after() runtime the fire-and-forget image-generation job runs inside.
GitHub Actions
lint, typecheck and test into a protected main, a nightly age-encrypted postgres backup, and a docs-to-wiki sync on push.

imn@dev:~/oss/wishka$ cat stack.md

Next.js 16React 19TypeScript strictTailwind v4Drizzle ORMPostgresBetter Authnext-intlVitest + pglite

imn@dev:~/oss/wishka$ cat highlights.md

SCHEMA
the surprise invariant is structural, not procedural: the wishes table has no reservation column at all, so there is no field to accidentally select into an owner-facing response.
LOCKS
reserving locks the wish row inside the same transaction as the owner's edit, delete and gift paths, and a partial unique index settles a concurrent double booking through the losing insert. deleting a booked wish orphans the reservation rather than deleting it, so its holder learns the gift disappeared.
SSRF
a hand-written guard parses ipv4 and ipv6 literals byte by byte to block private, link-local, cgnat and metadata ranges, re-validates every redirect hop, and documents dns-rebinding toctou as an accepted residual risk instead of leaving it unstated.
IMAGES
generation is a fire-and-forget job inside next's after(): the request returns immediately, the card polls a status column, and if nothing points at the upload by the time the job lands, the job deletes its own file.
BACKUP
a nightly action pg_dumps the database straight into age encryption using only a public key — the repo is public, so the workflow that produces the backup cannot decrypt it.
TESTS
79 vitest suites; every data-access test spins up an in-memory postgres via pglite and replays the full drizzle migration history, so unique indexes and check constraints are exercised against real sql rather than mocks.