Skip to content

Repository files navigation

Recipe book

A private log of the recipes you've actually cooked and would cook again — and, just as importantly, where each one came from.

The problem it solves: you cook from books, recipe sites and social posts, and the good ones get lost among the ones you tried once. This keeps the ones that worked in a single place you can search, with the ingredients and method pulled out for you and a link back to the original for when you want to see it as the author wrote it.

Built for one person. No accounts, no feed, no ratings from anyone else.

What it does today

  • Photograph a book — up to six pages, read together as one recipe, so a recipe that runs over a spread and onto the next page comes through whole. Title, ingredients, method, servings and timings are transcribed, along with the page number. Claude also finds the photograph of the finished dish and proposes a crop, which you can drag and resize before accepting. Any page that came out on its side can be turned before it's read. The page photos are kept too, so you can always check the transcription against the original.
  • Books are on a shelf. Photograph the cover once and every recipe you log from that book lands on it — the recipe page shows which book it came from, and the book's page shows everything you've cooked out of it.
  • Paste a link from a recipe site and have the same thing filled in. Sites that publish schema.org/Recipe markup — most of them do — are read directly from that markup, which costs no API call and is more accurate than reading the rendered page. Anything else falls back to Claude reading the page text.
  • Paste a social post. Instagram and TikTok serve a login wall to anything that isn't a signed-in browser, so the app tries the fetch, and when it's refused it says so plainly and offers the two things that do work: paste the caption, or add a screenshot. Either gets read the same way.
  • Pictures, swipeable. A main image and as many others as the source had. Any picture can be promoted to main; the main one is what the book shows.
  • Your own notes — the halve-the-sugar, needs-a-hotter-oven part. This is the bit you'll be glad of in a year, so it gets its own block on the page.
  • Search across titles, books, authors, notes and ingredients, because half of remembering a recipe is remembering it was the one with the anchovies. Tags, and filtering by them, are there too.
  • Tick things off while you cook. Ingredients and steps are tappable and strike through. Deliberately not saved — that's about where you are in this pan of food, not a fact about the recipe.
  • Installs to your phone's home screen and runs full-screen like an app.

Setup

You need a Neon database, an Anthropic API key, and a Vercel account. All three have free tiers that comfortably cover personal use.

1. Configure

npm install
cp .env.example .env.local

Fill in .env.local:

Variable Where it comes from
DATABASE_URL Neon dashboard → your project → Connection string (pooled)
ANTHROPIC_API_KEY console.anthropic.com → API keys
APP_PASSCODE You choose it. It's what unlocks the app on your phone.
AUTH_SECRET node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

2. Create the tables

npm run db:init

Safe to re-run; it only creates what's missing.

3. Run it

npm run dev

4. Deploy

Push to GitHub, import the repo in Vercel, and add the same four environment variables under Settings → Environment Variables. Deploy.

Then on your phone, open the Vercel URL, enter your passcode, and add it to your home screen — Share → Add to Home Screen on iOS, or ⋮ → Install app on Android. HTTPS is required for the camera, which Vercel gives you by default.

How it's put together

Piece Choice Why
Framework Next.js (App Router) One deployable for pages and API; first-class on Vercel
Database Neon Postgres over HTTP No connection pool to manage from serverless functions
Reading recipes Claude, server-side Handles a photographed page, a blog, and a caption with one model
Recipe sites schema.org/Recipe first Free, instant, and more accurate than reading the rendered page
Photos Base64 in Postgres One less service to wire up
Auth One passcode, signed cookie It's a single-user app on a public URL
Styling Tailwind v4
Type Fraunces + Schibsted Grotesk Both OFL, self-hosted at build, so no runtime call to Google

On the type. Fraunces (Undercase Type) for recipe names and notes, Schibsted Grotesk for everything else. Fraunces is loaded as a variable font so its optical-size axis can thin the strokes for a 36px title and thicken them for a 15px note, out of one file; SOFT and WONK give the serifs their character. Three classes select the cut — .masthead for the identity mark, .display for recipe names, .essay for running prose.

On the look. Warm oat paper rather than the mineral greys of a gallery, hairline rules, small letterspaced caps for labels, and a serif reserved for recipe names and your own notes. Food photography is the only strong colour on the page, which is the point: the book is a grid of dishes, and you recognise a meal before you read its name. The one accent is a herb green, used for chosen tags and the link back to the source.

A few decisions worth knowing about:

  • Your API key never reaches the browser. Reading happens in route handlers; the phone only ever posts an image, a URL or some text and gets JSON back.
  • Pages are stored before they're read, not after. A slow or failed transcription can't lose the photographs you just took, and each upload is its own small request rather than six pages inside one body — which is what used to cap how many pages you could send at once.
  • Orientation is baked in on upload. Phones record a landscape shot as a portrait frame plus an EXIF "turn this on the way out" tag, and not everything downstream honours it. The rotation is applied to the pixels when the photo is stored, and there's a Turn control on every picture for the cases that still come out sideways.
  • Turning a picture writes a new one. Photo bytes never changing under a given id is what lets the browser and the service worker cache them forever; rotating in place would serve everyone the old orientation until those caches expired. Turn four times and you're back where you started.
  • Photos are downscaled in the browser to 2000px before upload. That's more than a wine label would need, because a cookbook page is dense — small type, two columns, sometimes a whole spread — and the transcription is only as good as what it can read.
  • Reading is never load-bearing. Every path — photo, link, paste — ends at the same form. If the model fails, is misconfigured, or the page turns out not to be a recipe, you get a sentence explaining why and the form appears anyway with whatever was salvaged. You can always type a recipe in by hand.
  • The link is always kept, even when the page couldn't be read. Getting back to the source is half the point of the log.
  • Pictures from the web are copied, not hotlinked. Sites reorganise and CDNs expire; a saved recipe shouldn't depend on someone else's server still answering in two years.
  • Ingredients and method are stored as groups, each with an optional heading, because real recipes have "For the sauce". A recipe without headings is one group with a null heading, so nothing downstream needs a special case.
  • Tags are stored as ids, not labels (one_pot, not "One pot"), so the wording can change later without rewriting your history.
  • Nothing is cached offline except the shell and the photos. A stale ingredient list would be worse than an error message when you're halfway through cooking from it. Photos are the exception, and a safe one: a photo is written once under an id that never points at anything else.
  • The server refuses to fetch private addresses. The link comes from you, but the URL reader makes the server fetch whatever it's given, so it won't be pointed at localhost or a metadata endpoint.

Layout

src/
  app/
    page.tsx              the book — grid, search, tag filter
    add/                  capture → read → crop → form
    recipe/[id]/          one recipe, and its edit form
    books/, book/[id]/    the shelf, and everything cooked from one book
    login/                passcode gate
    api/
      extract/photo/      Claude reads photographed pages, and finds the dish
      extract/url/        schema.org first, Claude on the page text otherwise
      extract/text/       Claude reads a pasted caption
      photos/             upload, serve, and turn pictures
      books/              the shelf
      recipes/            CRUD
      auth/               login and lock
  components/
    AddRecipeFlow.tsx     the capture flow and its fallbacks
    CropAdjuster.tsx      drag and resize the proposed crop
    RecipeForm.tsx        one form, shared by add and edit
    BookPicker.tsx        pick a book off the shelf, or add one with its cover
    RecipeBody.tsx        tickable ingredients and steps
    ImageCarousel.tsx     scroll-snap picture strip
  lib/
    extract.ts            the shared prompt, schema and Claude call
    fetch-page.ts         fetching, schema.org parsing, image discovery
    recipes.ts            queries and input validation
    books.ts              the shelf, and matching a book you've logged before
    photos.ts             storing uploads and remote pictures
    image.ts              browser-side downscaling
    taxonomy.ts           source types and tags
    auth.ts               cookie signing
  proxy.ts                the passcode gate, in front of everything
scripts/
  init-db.mjs             schema
  gen-icons.mjs           draws the PWA icons

Where this goes next

  1. Favourites per book. The shelf knows which recipes came from which book; marking a few per book as the ones worth going back to is a small addition on top of the tags that already exist.
  2. A "what can I cook?" view. The ingredients are already stored as lines per recipe; parsing quantities out of them would let the book answer "what can I make with what's in the fridge".
  3. Cook mode — keep the screen awake, one step at a time, big type.
  4. Scaling. Servings is free text today. Parsed quantities would let a recipe be doubled or halved in place.
  5. Server-side search. The recipes table already carries a generated tsvector and a GIN index, and GET /api/recipes?q= uses it. The list page currently filters on the client instead, which is instant and works well for a personal collection; the server path is there for when it isn't.

About

Recipe saving app

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages