Recipe database and home inventory database, tied together. A personal pantry-and-cooking system: track what you own, where it lives, what it cost, and what you can cook with it.
Cubby is a personal system β built for my own household, not a product for strangers. Its north-star is the recipe β inventory tie: knowing what I can actually cook from what I physically own, where it lives, and what it costs. Most tools do recipes or a pantry list; Cubby joins the two, so "what can I make tonight, and what would it cost?" becomes a query instead of a guess.
It's three things at once: an earnest daily-use home utility, a playground for a modern stack (TanStack Start, Cloudflare Workers, Rust/WASM, agentic AI), and a place to hold a high engineering bar on something I actually use.
Cubby is not:
- A multi-tenant SaaS β no public sign-ups, billing, or tenant-isolation work
- Cross-platform β mobile is iOS-only by design
- A social app β no feeds, public recipe sharing, or community
- A commerce tool β no in-app buying, ordering, or cross-store price-shopping
Inventory
- Add/edit/move inventory items across hierarchical locations
- Barcode scan for quick capture (mobile-optimized)
- Bulk edit and move
- Gallery, table, and visualization (treemap, sunburst) views
Products
- Specific items (UPC, manufacturer, price, nutrition) or
misc:placeholders - Multi-unit mappings (volume β weight β price) for cross-unit conversions
- Optional link to an ingredient and to a USDA food entry
Recipes
- Multi-section recipes with nested ingredients
- Ingredients can be other recipes (composition)
- Side-by-side recipe comparison
- Cost rollups via product unit mappings
USDA
- Full USDA FoodData Central database loaded into a sibling service
- Browse, search, and link products by UPC or NDB code
Locations
- Tree structure (house β room β shelf β bin)
- Interactive graph, treemap, sunburst views
- Printable QR-code shortcode labels
Images
- S3/R2-backed image upload with presigned URLs
- Linked to products, locations, or recipes
Analytics
- Donut, treemap, sunburst, and network visualizations across products, inventory, and ingredients
- Category audits + data-problem detection
Auth & Audit
- Better-Auth sign-up/login with API keys
- Activity log across all entities
- Soft delete on all major entities (no restore β by design)
Mobile (PWA, iOS)
- Installable PWA with splash screens
- Touch-friendly cards and immersive barcode scanner
- Offline support and swipe gestures are planned β see Roadmap
- App: TanStack Start (Router + Server) Β· React Β· TailwindCSS Β· shadcn/ui
- API: tRPC + TanStack Query
- Data: Drizzle ORM Β· PostgreSQL Β· Hyperdrive (edge pool)
- Auth: Better-Auth (
@daveyplate/better-auth-uifor routed UI) - Edge: Cloudflare Workers + Wrangler
- WASM:
@cubby/recipebridgewraps Rust ingredient-parser - Storage: Cloudflare R2 (S3-compatible) for images
- Charts: Nivo (bar, pie, treemap, sunburst, calendar, line) + d3-force, d3-hierarchy
- Tooling: Biome (lint + format) Β· Vitest (unit/integration) Β· Playwright (E2E) Β· IntegresQL (test DB isolation)
- Observability: OpenTelemetry β Jaeger (dev only) Β· Sentry
| Path | Package | Role | Runtime | Deploys to |
|---|---|---|---|---|
| apps/web | @cubby/web |
Main app β TanStack Start + tRPC + Drizzle | Cloudflare Workers | Worker cubby Β· DB via Hyperdrive β Postgres |
| apps/upc-lookup | @cubby/upc-lookup |
UPC barcode lookup API β Hono + D1 | Cloudflare Workers | Worker upc-lookup Β· https://upc-lookup.nicky.workers.dev |
| apps/usda-api | @cubby/usda-api |
USDA FoodData Central API β Hono + D1/R2 bundles | Cloudflare Workers | Worker usda-api Β· https://usda-api.nicky.workers.dev Β· D1 search index + R2 NDJSON payload bundles |
| Path | Package | Role | Consumed by |
|---|---|---|---|
| packages/wasm | @cubby/recipebridge |
WASM bindings β built from recipebridge/ Rust source via pnpm run wasm |
web |
| packages/usda-contract | @cubby/usda-contract |
ts-rest endpoint contract for the USDA API | web, usda-api |
| packages/usda-schemas | @cubby/usda-schemas |
Shared Zod schemas for USDA entities | web, usda-api |
| packages/schemas | @cubby/schemas |
Cross-app Zod schemas | web |
| packages/shared | @cubby/shared |
Shared utilities | web |
| recipebridge/ | (Rust source) | Source for the ingredient-parser WASM shim | Built into packages/wasm |
Request flow:
Router (tRPC) β Service (optional, enrichment only) β Repo (data access) β Database
- Routers use the CRUD factory (
createEntityCrudProceduresfromcrud-factory.ts) for standard CRUD operations. - Services exist only when entities need enrichment (e.g., USDA food data). Otherwise routers call repos directly.
- The
Databasetype is opaque β only repos can callgetDb(db)to unwrap it. This enforces the layered architecture at the type level.
See CLAUDE.md for the prescriptive rules (branded IDs, soft delete, required helpers, React hooks pitfalls).
Recipes have multiple sections, each of which has Ingredients and an amount. Ingredients can also be other recipes.
Products have multiple unit mappings (each of which contain 2 amounts). Products can also point to an ingredient.
USDA Food database is loaded, loosely linked to products based on the products NDB number or UPC code.
Products can be inventoried β an Inventory Entry specifies the amount of a given Product at a given Location.
erDiagram
Recipe ||--o{ RecipeSection : "has sections"
RecipeSection ||--o{ RecipeSectionIngredient : "has ingredients"
Ingredient ||--o{ RecipeSectionIngredient : "used in"
Recipe ||--o| Ingredient : "can be ingredient"
Product ||--o{ InventoryEntry : "inventoried as"
Product }o--|| Ingredient : "points to"
Location ||--o{ InventoryEntry : "contains"
Image ||--o{ Product : "linked to"
Image ||--o{ Location : "linked to"
Image ||--o{ Recipe : "linked to"
Product }o--o| usda_food : "linked by UPC/NDB"
Prereqs: Node (see .nvmrc, currently v24), pnpm (pinned in package.json β packageManager: pnpm@10.33.4), Docker, wrangler (for CF Workers work).
# 1. Install
pnpm install
# 2. Env
cp apps/web/.env.example apps/web/.env
# Edit apps/web/.env β see "Environment Variables" below
# 3. Local services (Postgres 17 + IntegresQL + Jaeger)
docker-compose up -d
# 4. DB schema
pnpm --filter @cubby/web run db:migrate
# 5. Build the WASM shim (one-time, or whenever recipebridge/ changes)
pnpm run wasm
# 6. Dev server
pnpm run devApp: http://localhost:3000 Β· Jaeger: http://localhost:16686
Required keys (see apps/web/.env.example for the full file):
| Key | Purpose |
|---|---|
BETTER_AUTH_SECRET |
Auth signing secret |
DATABASE_URL |
PostgreSQL connection (defaults to local docker-compose) |
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_ENDPOINT / R2_BUCKET_NAME / R2_PUBLIC_URL |
Image storage |
USDA_API_URL |
USDA service URL (defaults to http://localhost:8787/ for local Wrangler dev) |
UPC_LOOKUP_API_URL / UPC_LOOKUP_API_KEY |
UPC lookup worker |
NOTION_API_KEY |
(optional) Project Tracker dashboard |
OTEL_EXPORTER_OTLP_ENDPOINT |
(optional) OTLP traces β Jaeger |
Claude Code can run parallel sessions, each in its own git worktree under
.claude/worktrees/<name>. A few things to know:
- Fresh worktree setup:
pnpm install && pnpm run wasm. Gitignored env (apps/web/.env,.env.local) is copied in automatically via .worktreeinclude;node_modulesand the gitignored WASM package (packages/wasm/*) are not, so build them once. - Builds are shared, not cold. The
wasmscript pointsCARGO_TARGET_DIRat a shared cache (~/.cache/cubby/recipebridge-target), so worktrees reuse the compiled Rust deps β a worktreepnpm run wasmis an incremental build, not the ~90s cold one, and there's no 1.3GBtarget/per worktree. - WASM never silently drifts. scripts/ensure-wasm.mjs
rebuilds the gitignored WASM only when a source it's built from is newer than the
built binary. "Sources" is
recipebridge/plus every local path-dependencycargo metadatareports (source: null) β notably the ingredient-parser working copy the global~/.cargo[patch]redirects to, so editing the parser locally is caught too (no patch β onlyrecipebridge/, same as CI). It runs onpnpm dev(so a local parser edit rebuilds on the next dev start) and ongit pull/git checkout(huskypost-merge/post-checkout, for a pulled rev bump or branch switch) β on the main checkout too. cargo does the real incremental compile; this is just the staleness gate that skips the ~10s wasm-bindgen/opt when fresh. - Ports. The main checkout is always
:3000(vite.config.tsusesstrictPort, so it fails loudly rather than drifting). Worktree dev servers auto-pick a free port β the preview harness viaautoPort(injectsPORT), or a terminalpnpm devvia vite's auto-increment. - Previewing a worktree: start a session with the worktree folder itself
selected as the project (
<repo>/.claude/worktrees/<name>), not by entering a worktree from inside the main-rooted session β preview resolveslaunch.jsonfrom the folder you opened, so opening the worktree serves its branch. - Shared services: docker-compose (Postgres/IntegresQL/Jaeger) binds fixed host
ports β
docker-compose up -donce from any checkout and all worktrees reuse them fortest/test:e2e. - β Shared prod DB: every worktree's
DATABASE_URLis the same prod Neon instance (dev DB is prod).db:pushand data changes from one worktree are visible everywhere and hit prod β coordinate schema changes across parallel work. - Editing
recipebridge/Rust source β or the patched sibling ingredient-parser checkout β is picked up automatically on the nextpnpm dev(see "WASM never silently drifts" above);pnpm run wasmforces it. Needs the rust toolchain + the global cargo patch + the sibling ingredient-parser checkout.
| Command | What it does |
|---|---|
pnpm run dev |
Start all dev servers (Node, not Workers) |
pnpm run check |
Biome (lint + format check) + parallel typecheck |
pnpm run typecheck |
Typecheck with tsgo (TS 7.0 preview, fast) |
pnpm run typecheck:stable |
Typecheck with stable TypeScript (fallback) |
pnpm run format:write |
Auto-fix formatting (Biome) |
pnpm run test |
Vitest unit + integration |
pnpm run test:e2e |
Playwright E2E (uses IntegresQL) |
pnpm --filter @cubby/web run db:migrate |
Apply Drizzle migrations |
pnpm --filter @cubby/web run build:cf |
Build for Cloudflare Workers |
pnpm --filter @cubby/web run preview:cf |
Run the Workers build locally |
pnpm --filter @cubby/web run deploy:cf |
Deploy to Cloudflare Workers |
pnpm run wasm |
Rebuild @cubby/recipebridge from Rust source |
In dev, await __jsProfile(5000) in the browser console captures a CPU flame summary (the hottest main-thread frames over the next N ms) β it catches "every measurement is fast but the page is slow" jank that React's profiler can't see (commit-phase / native / third-party work).
| Suffix | Purpose | Runner |
|---|---|---|
*.unit.test.ts |
Unit tests | Vitest |
*.integration.test.ts |
Integration tests (real DB via IntegresQL) | Vitest |
*.spec.ts |
E2E tests | Playwright |
E2E tests use IntegresQL to spin up fresh databases per test β see memory notes for SSR hydration gotchas and the e2e-helpers.ts shared helpers.
| Pattern | Example | Used For |
|---|---|---|
*.service.ts |
product.service.ts |
Service layer (enrichment) |
*-helpers.ts |
database-helpers.ts |
Utility helpers |
*-utils.ts |
location-utils.ts |
Utility functions |
types.ts / internal-types.ts |
repo/product/types.ts |
Local type definitions |
What deploys where lives in the Monorepo Layout table. This section covers the non-trivial internals of the main app's CF Workers deploy. (upc-lookup and usda-api are also Cloudflare Workers.)
The dev server is plain Node via vite dev. Production = CF Workers.
| File | Purpose |
|---|---|
| apps/web/src/cf-server.ts | Worker entry β wraps each request with withRequestDb() |
| apps/web/src/server/db.ts | Per-request pg.Client via AsyncLocalStorage (CF) or module-level pool (dev) |
| apps/web/src/lib/recipebridge-cf.ts | WASM wrapper using ?init pattern for CF |
| apps/web/wrangler.jsonc | Worker config (name, vars, Hyperdrive, compat flags) |
| apps/web/vite.config.ts | cfWasmPlugin() + __CF_WORKERS__ define for dead-code elimination |
Key constraints:
- Hyperdrive pools TCP connections at CF's edge. The connection string comes from
env.HYPERDRIVE.connectionString(not a secret). Uses standardpg.Client. - Per-request
pg.ClientviawithRequestDb()+AsyncLocalStorage, even though Hyperdrive reuses underlying connections. - WASM uses
?initbecausevite-plugin-wasmdoesn't apply to CF's SSR environment.cfWasmPlugin()redirects@cubby/recipebridgetorecipebridge-cf.ts. __CF_WORKERS__define eliminates module-level Pool creation from the CF build.- OTel disabled in production β only runs in dev via
instrument.server.mjs. - Secrets via
wrangler secret put BETTER_AUTH_SECRET(etc.) β seewrangler.jsoncfor the full list.
Better-Auth via better-auth/tanstack-start. Routed UI by @daveyplate/better-auth-ui.
| File | Purpose |
|---|---|
| apps/web/src/lib/auth.ts | Server config |
| apps/web/src/lib/auth-client.ts | Client (useSession and friends) |
| apps/web/src/routes/api/auth/$.ts | API catch-all route |
| apps/web/src/routes/auth.$authView.tsx | Auth UI (sign-in/up) |
| apps/web/src/routes/_authenticated/account.$accountView.tsx | Account UI |
Visit http://localhost:3000/api/auth/session while running the app to inspect the current session.
| Type | Example | Characteristics |
|---|---|---|
| Specific Item | "Kraft Macaroni & Cheese" | Has UPC, manufacturer, price, nutrition. Created via barcode scan. |
| Misc Collection | misc:random cables |
Opaque placeholder. Just a name, no details. Prefix with misc:. |
@cubby/recipebridgewraps Rust WASM from ingredient-parser.- Client-side:
wasmfrom~/lib/wasm(sync). - Server-side:
wasmServer(async, auto-initializing). - Supports chained conversions via graph algorithms β e.g.
2 cups β $5.00 β 333gusing a product's unit mappings.
- Contract:
@cubby/usda-contractdefines endpoints with Zod schemas (ts-rest). - Schemas:
@cubby/usda-schemasfor shared entity types. - Client: apps/web/src/server/clients/usda.ts wraps the ts-rest client.
- Router: apps/web/src/server/api/routers/usda.ts.
- Service layer processes USDA portion data through WASM for conversions.
usdaClient.findFood({ kind: "upc", gtin_upc: "123456789012" });
usdaClient.findFood({ kind: "ndb", ndb_number: 12345 });
usdaClient.listFoods(nameFilter, dataTypeFilter, sort, pagination);
usdaClient.getFoodSummaryByID(fdcId);Framed as Now / Next / Later (no dates β it's a personal project). Canonical plans live in docs/plans/; the links are pointers, not summaries, so they don't rot.
- Meal planning v1 β plan recipes onto a calendar (week + table views), scale each per meal, and a display-only shopping list (aggregated need vs. on-hand inventory, with a per-meal breakdown). Cook-and-consume inventory deduction was deliberately scoped out β it lives under Meal planning v2 below β docs/todos.md
- Nothing active β next focus will be promoted from Next below.
- AI deepening β smarter Ask Cubby and better photo capture, building on the shipped "what can I make tonight?" (
find_cookable_recipes) tool β docs/todos.md - Nutrition & cost intelligence β price-per-nutrient, daily-value %, and nutrient-density comparisons via WASM conversion extensions β docs/todos.md
- Meal planning v2 β cook-and-consume inventory deduction (Phase 4), expiration-aware suggestions, FEFO consumption, meal templates, nutrition goals
- WASM deep cuts β batch recipe parsing, custom unit aliases, inventory depletion preview
- Location drag-drop in the tree view
- Engineering backlog β document test-placement criteria; persist scraped/Notion hero images on the server import path
- CLAUDE.md β agent rules, anti-patterns, required helpers
- docs/style-guide.md β Tailwind/CSS conventions
- docs/todos.md β work list
- docs/plans/ β design docs for in-progress / upcoming features
- docs/combobox-consolidation.md β component consolidation notes