A bilingual (English / Spanish) personal recipe collection focused on Costa Rican home cooking. It's a Next.js web app and installable PWA with faceted browsing, full-text search, meal planning, a kitchen-friendly cook mode, metric ⇄ imperial conversion, and AI-assisted recipe import — backed by Supabase.
Live: https://recipe-app-blush-eta.vercel.app
Part of the Finca Colibrí family (shop, recipes, cabins, finca hub): see the family map.
Browse & discover
- Faceted Browse (category, region, cuisine, dietary, difficulty, max time) with sortable results
- Landing pages per category and region (region is country-level max — e.g. "Costa Rica" is a region, "Costa Rican" is a cuisine)
- Keyword Search powered by Postgres full-text search (English + Spanish), trigram fuzzy matching, and accent-insensitive lookup
- Meals — curated multi-recipe menus with one combined, aisle-sorted shopping list
In the kitchen
- Cook Mode — full-screen, step-by-step, with swipe/tap navigation and a screen wake lock
- Step timers with an audible + vibration alarm when they finish
- Servings scaling that rewrites both the ingredient list and inline quantities in the steps
- Metric ⇄ imperial toggle — converts amounts (cups ↔ ml, oz/lb ↔ g/kg…) and oven temperatures (°F ⇄ °C)
- Kitchen-friendly print view
Personal layer (per-device, localStorage — no account needed)
- Favorites, recently-viewed, dated cook-notes, ingredient check-off, and unit-system preference
AI-assisted (Anthropic)
- Import a recipe from a URL, pasted text, or a photo
- Identify — "what can I cook?" from pantry ingredients
- Per-recipe chat (substitutions, tips, scaling) and on-demand translation to Spanish
- Every AI call runs on Claude Haiku 5.5 (
claude-haiku-5-5); see ARCHITECTURE.md
Platform
- Installable PWA with an offline fallback (
manifest.json+ hand-authored service worker) - Bilingual throughout via a locale-prefixed route tree (
/en,/es) - ThumbHash blur placeholders for images (no layout shift, no grey flashes)
- ISR-cached pages, JSON-LD structured data, sitemap, and robots
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router) · React 19 · TypeScript (strict) |
| Styling | Tailwind CSS v4 |
| Data | Supabase (Postgres 17 + Storage) |
| Search | Postgres FTS + pg_trgm + unaccent |
| AI | Anthropic SDK (@anthropic-ai/sdk) · Claude Haiku 5.5 for every call |
| Images | Supabase Storage · sharp · thumbhash |
| Rate limiting | Upstash Redis (optional; in-memory fallback) |
| Observability | Vercel Analytics + Speed Insights |
| Testing | Vitest (unit) · Playwright (e2e) |
| Hosting | Vercel |
Requires Node ≥ 20.19.
# 1. Install dependencies
npm install
# 2. Configure environment
cp .env.local.example .env.local
# edit .env.local — see "Environment variables" below
# 3. Run the dev server
npm run dev
# open http://localhost:3000Reading and browsing recipes only needs the two public Supabase variables. AI features need ANTHROPIC_API_KEY; writing/importing needs ADMIN_PASSWORD (and, once the RLS-lock migration is applied, SUPABASE_SERVICE_ROLE_KEY).
| Command | What it does |
|---|---|
npm run dev |
Start the dev server |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
npm test |
Run the Vitest unit suite |
npm run test:watch |
Vitest in watch mode |
npm run test:e2e |
Run Playwright e2e tests |
Copy .env.local.example and fill in. NEXT_PUBLIC_* values are shipped to the browser; everything else is server-only.
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase project URL (public) |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Supabase anon key (public) |
SUPABASE_SERVICE_ROLE_KEY |
For writes | Server-only key; required for writes once the recipes-table RLS lock is applied. Never expose with a NEXT_PUBLIC_ prefix. |
ADMIN_PASSWORD |
For writes/AI-writes | Gates write/import/admin endpoints and admin login, and signs the admin session cookie (so it is also required for the finca sign-in). In production, writes default-deny until this is set. |
FINCA_ID |
Optional | Finca whose editors/admins may sign in (default colibri). Passed to public.finca_can_write(p_finca_id). |
ANTHROPIC_API_KEY |
For AI features | Import, identify, chat, translate, and the Cocina chat (same key, nothing new to set) |
HUB_ORIGINS |
Optional | Comma-separated origins of the finca hub allowed to call POST /api/cocina/chat (only that route; other routes keep the site-origin CORS). Default https://sandgraal.github.io,http://localhost:5173,http://localhost:4173 |
AI_MODEL |
Optional | Model for every AI call: import, identify, chat, translate, Cocina chat (default claude-haiku-5-5, no date suffix) |
COCINA_CHAT_MODEL |
Optional | Model for the Cocina chat only; wins over AI_MODEL (default: AI_MODEL) |
NEXT_PUBLIC_SITE_URL |
Optional | Canonical origin for CORS, metadata, sitemap, JSON-LD (defaults to the prod URL) |
UPSTASH_REDIS_REST_URL |
Optional | Shared-store rate limiting for public AI routes; falls back to an in-memory limiter if unset |
UPSTASH_REDIS_REST_TOKEN |
Optional | Token for the above |
One account across the family: the Finca Colibrí email + password (Supabase Auth users, sign-ups disabled) is the default way to unlock editing.
- The lock icon opens a sign-in form. The browser signs in with the anon client (
signInWithPassword, no persisted session) and POSTs{ accessToken }to/api/auth/login. - The route verifies the token with Supabase (
auth.getUser), then callsrpc('finca_can_write', { p_finca_id: FINCA_ID })as that user. Only admins/editors of the finca pass; non-members, read-only roles and bad tokens get 403/401 and no cookie. - On success it sets the same signed httpOnly admin cookie as before (the cookie is the only session; the browser then calls
signOut({ scope: 'local' })so no Supabase tokens stay in localStorage). Logout clears the cookie.
Fallbacks, unchanged: POST /api/auth/login with { password } (the UI has a small "Use admin password" link), and Authorization: Bearer <ADMIN_PASSWORD> on write routes for automation such as the recipe publish skill.
Env validation is intentionally lenient (src/lib/env.ts): it warns on missing/malformed values rather than crashing, so a misconfigured deploy still boots and individual features degrade gracefully.
Supabase Postgres project ref: cjnwvxtomwyszcfuvgpw. Schema lives in supabase/migrations/ and is applied to the project via the Supabase CLI or dashboard (SQL editor / MCP). Highlights:
…_recipes_baseline.sql—recipestable…_lock_recipes_writes.sql— RLS lock (writes go through the service-role key)…_search_recipes_function.sql,…_search_fts.sql,…_search_fts_ranking.sql— search RPC → FTS with ranking…_meal_groups.sql,…_meal_groups_shopping_list.sql— meal planning…_recipe_taxonomy.sql— structuredcategory/region/dietary/difficulty/ time fields + check constraints…_recipe_indexes.sql— GIN / btree indexes for facets and sorting…_image_thumbhash.sql—image_thumbhashcolumn for blur placeholders
Extensions used (in the Supabase extensions schema): pg_trgm, unaccent.
src/
app/
[lang]/ Locale-prefixed pages (en / es)
browse, search, category/[category], region/[region]
meals, meals/[slug]
recipes/[id], recipes/[id]/edit, recipes/new
identify, import, privacy, terms
api/ Route handlers
recipes, recipes/[id] CRUD
import/{url,text,photo} AI import
identify, recipe-chat, translate AI features
cocina/chat Hub Cocina chat (SSE, finca editors)
auth/{login,logout} Admin session
admin/backfill-taxonomy Maintenance
layout.tsx, sitemap.ts, robots.ts, manifest.json
components/ UI (RecipeCard, FilterRail, CookNotes, …)
lib/ Domain logic
taxonomy, browse, recipes, search helpers
scale, convert, time Quantities / units / time
schemas, requestBody, ssrf, cors Validation & safety
hubCors, cocinaChat Cocina chat: hub CORS, prompt, tool, schemas
adminAuth, adminSession, rateLimit Auth & throttling
i18n, site, supabase, thumbhash* Infra
use*.ts Client hooks (favorites, cook-mode, …)
public/ Icons, service worker (sw.js), offline.html
supabase/migrations/ SQL schema
e2e/ Playwright specs
Unit tests (Vitest) live next to the code they cover in src/lib/*.test.ts and focus on the pure domain logic — quantity scaling, unit conversion, taxonomy derivation, search/browse helpers, i18n coverage, SSRF guards, and request validation. End-to-end smoke tests (Playwright) live in e2e/.
npm test # unit
npm run test:e2e # end-to-endHosted on Vercel; pushing to main triggers a production deploy. See DEPLOY.md for first-time setup and the environment variables to configure in the Vercel dashboard.
- ARCHITECTURE.md — how it fits together: rendering model, read/write data flow, search, the taxonomy/region rules, and the write-auth model.
- DEPLOY.md — Vercel setup and environment configuration.