🍷🧀 Personal project

Cupboard
our digital pantry

A cozy side project — a shared digital pantry collection of wines, liquors, recipes, cheeses and restaurants. What started as a simple note-keeping idea turned into a full-stack platform with AI-powered features and live integrations.

Cupboard app on iPhone

The problem

We kept discovering wines and restaurants and forgetting them by the time we got home. Same with cheeses, recipes and places we loved. A notes app felt too unstructured and limited — we wanted something curated and shared.

The solution

A content-driven platform where we catalog everything we love about food and drink. Wines and liquors connect to recipes, restaurants get their menus pulled in automatically, and everything is queryable by taste profile, type, origin or cuisine. Backed by Sanity.io, with live data from Systembolaget and AI-extracted menu data for restaurants.

Technical highlights

🍷

Systembolaget integration

The GCP functions (written in Go) query Sweden's national alcohol retailer for live product data — pricing, taste profiles, grapes, packaging and more. Results are cached in memory for 1 hour.

📸

AI wine label recognition

Point your phone at a wine bottle and the app identifies it. A two-stage OpenAI pipeline first extracts the wine name from the label image (GPT-4o vision), then searches Systembolaget using the web-search model. Falls back to Google Vision API and Tesseract.js OCR for reliability.

🤖

Discord bot

We can add wines on the spot while still sitting at a restaurant. The bot runs on a separate server and calls the /addwine/ and /addimage/ Go Cloud Function endpoints — same API the web app uses. It also lets us drop photos straight into the site gallery, uploading through the same Go functions into Sanity. By the time we get home, the wine — or the photo — is already on our website.

🎨

AI image generation in Studio

Custom Sanity Studio plugin that adds an OpenAI-powered image generation field. When adding a recipe or restaurant, you can generate an illustrative image directly inside the CMS without leaving the editor.

🖼️

Dynamic OG image generation

Every page gets a unique social-share image, rendered server-side at request time with Satori (JSX → SVG) and @resvg/resvg-wasm. No native binaries involved, so it runs cleanly in the Alpine Docker image we ship to production.

🍽️

Automated restaurant menu lookup

Publishing a restaurant in Sanity triggers a lookup service that crawls the restaurant's own website and extracts its menu — dishes, categories, prices — along with signals like cuisine and atmosphere, writing the structured result straight back into the CMS. Editors can still override or clear it manually.

Architecture

// User browses the frontend

React + React Router rendered server-side by Express

// Content comes from Sanity

Node server queries Sanity CMS via GROQ

// Live product data fetched from Systembolaget

Server calls Go Cloud Functions → queries Systembolaget

// Adding a wine from a restaurant via Discord

Discord slash command → Go Cloud Function → writes to Sanity

// Dropping a photo into the gallery via Discord

Upload file in discord server → Go Cloud Function → uploads image to Sanity gallery

// Publishing a restaurant in Sanity

Sanity Studio action → lookup service crawls restaurant's website → extracts menu → writes back to Sanity

// Publishing a wine or liquor in Sanity

Sanity Studio action → Go Cloud Function → queries Systembolaget → matches Systembolaget product → writes back to Sanity

// Sharing a page on social

Express renders JSX with Satori → SVG → PNG via @resvg/resvg-wasm, no native binaries

Tech stack

Frontend

  • React
  • React Router
  • TypeScript
  • Tailwind CSS

Backend

  • Node.js + Express (SSR)
  • Google Cloud Functions (Go)
  • Satori + @resvg/resvg-wasm (OG images)

CMS

  • Sanity.io
  • Custom GROQ queries
  • Custom Studio plugins
  • Custom Studio publish actions

AI

  • OpenAI GPT-4o (wine recognition)
  • OpenAI (image generation)
  • OpenAI (menu extraction)
  • Google Vision API
  • Tesseract.js (OCR)

Integrations

  • Systembolaget
  • Google Maps
  • Discord bot

Infrastructure

  • Google Cloud Functions
  • Google Cloud Secret Manager
  • Docker

What I learned

Go for serverless functions

Writing the Cloud Functions in Go was a deliberate choice — fast cold starts, tiny binaries, and one shared API that the web app, Sanity Studio actions, and the Discord bot all call to access the same underlying functions.

Sanity as a two-way system

Already knew Sanity going in. What I picked up here was custom publish actions, webhooks, and Go functions patching data back into Sanity — not just reading content out, but writing into it too.

Using AI for data extraction

Restaurant menus rarely come as clean data. The Go functions scrape a restaurant’s website and let OpenAI turn that mess into a structured menu — dishes, categories, prices, cuisine, atmosphere — written straight back into Sanity.

See it live

Cupboard is live and we actually use it.