All projects

Great Whiskey Narrative

Know the bottle before you pour. Point a camera at a label and get the distillery, region, tasting notes, and what the bottle typically costs — then keep track of what you have drunk. The web library is live; the iOS app is in build.

What it does

  • A 370-bottle library, filterable by brand, category, country, region, and flavour, each bottle with its own illustration
  • Label scanner — send a photo of a label and the API matches it against the catalogue
  • Food pairings and a Whisky 101 guide for people who are still working out what they like
  • Tasting tracks: a game layer where tracks, tiers, badges, XP, rank, the passport, flavour mastery, and the cabinet are all computed from your collection on every render, never stored — so there is nothing to migrate and no way for the two to disagree
  • Bottles are graded by price rather than banded — six steps at $50, $100, $200, $600, and $2,500, which splits the shelf evenly instead of dumping a third of it into "legendary"

Nothing about your collection leaves the browser

Bottles you mark owned, tasted, or wishlisted live in localStorage. There are no accounts. To move a collection between devices, the site mints a transfer link that carries the bottles in the URL fragment — fragments are never sent to a server, so the data passes directly between the two devices. Bottles travel as a hash of their id rather than an index, so an old link keeps working after the library grows.

Tech stack

Three pieces ship separately out of one repo. The public site is a static Astro build with React islands — 121 pages and 326 Vitest tests — hosted on GitHub Pages, with 370 bottle illustrations served as lazy-loaded WebP. The API is a TypeScript service on Railway handling label scans, the versioned catalogue, and health probes; accounts and collection sync are built but stay off unless configured. The mobile app is Expo and React Native, shipping through EAS, and it reads a built catalogue artifact rather than the site's JSON so the two can version independently.

One lesson worth keeping: a green test suite is not proof the server starts. Jest transpiles ESM to CommonJS and tsc --noEmit only reads type declarations, so a package that is CommonJS at runtime can pass both and still fail Node's named-export check the moment the server loads it. A boot test that spawns the real entry point and hits its health endpoint now covers that gap.