Croft

Developer Documentation

Architecture Overview

Croft is a monorepo with four packages managed by pnpm workspaces and Turborepo:

PackageNameDescription
apps/web@croft/webReact Router v7 on Cloudflare Workers — UI + API
packages/db@croft/dbDrizzle ORM schema, migrations, seed scripts
packages/shared@croft/sharedShared types, Zod schemas, constants
agent@croft/agentMeal plan generation scripts, Notion client, API client

Data Flow

Notion (recipes) ──→ generate-plan script ──→ Croft API ──→ D1 Database
                                                                  ↓
                                                             Web App UI

The generate-plan script reads recipes from Notion via the Notion API, syncs them to the database, then generates a deterministic 4-week plan. All data flows through the web app API.

Tech Stack

ComponentTechnology
FrameworkReact Router v7 (SSR on Workers)
DatabaseCloudflare D1 + Drizzle ORM
CSSTailwind CSS v4
Drag & drop@dnd-kit/core (touch-friendly)
Push notifications@block65/webcrypto-web-push
Monorepopnpm workspaces + Turborepo
IDsULIDs (time-sortable)

API Reference

UI Routes (protected by Cloudflare Access in prod)

MethodPathDescription
GET/api/preferencesHousehold members with dietary info
POST/api/meals/searchSearch meals by name
POST/api/meal-plans/:id/swapSwap two meal entries within a week
POST/api/meal-plans/:id/replaceReplace a meal entry with a different meal
POST/api/meal-plans/:id/approveApprove a week → generates shopping list
POST/api/meal-plans/:id/clearClear all entries from a week
POST/api/shopping-list-items/:idToggle check/uncheck on a shopping list item
GET/api/chat/unprocessedUnprocessed chat messages
POST/api/chat/sendSend a chat message (content, contextWeek)

Push Notification Routes

MethodPathDescription
GET/api/push/vapid-keyReturns VAPID public key for client subscription
POST/api/push/subscribeSave a push subscription (endpoint, keys, deviceName)
DELETE/api/push/subscribeRemove a push subscription by endpoint

Calendar Routes

MethodPathDescription
GET/api/calendar/connectRedirects to Google OAuth consent screen
GET/api/calendar/callbackOAuth callback — exchanges code for tokens
POST/api/calendar/disconnectClears stored OAuth tokens
POST/api/calendar/selectSelect which calendar to use (calendarId)
GET/api/calendar/events?from=&to=Fetch calendar events for date range

Agent Routes (Bearer token auth)

All agent routes require Authorization: Bearer <AGENT_API_KEY>.

MethodPathDescription
GET/api/agent/cyclesGet current/upcoming cycle
POST/api/agent/cyclesCreate a new 4-week cycle (startDate, endDate)
POST/api/agent/mealsCreate/update meals with ingredients (deduplicates by name)
POST/api/agent/meal-plansCreate a weekly plan with entries (cycleId, weekStart, entries[])
POST/api/agent/notificationsSend push notification (type, message)
POST/api/agent/chat/mark-processedMark chat messages as processed
GET/api/agent/recent-meals?weeks=NMeals planned in the last N weeks

Data Model

Core Tables

meals

ColumnTypeNotes
idtext (PK)ULID
nametextMeal name
notionPageIdtext?Link to Notion recipe
descriptiontext?Short description
prepTimeMinsinteger?Prep time in minutes
cookTimeMinsinteger?Cook time in minutes
difficultytext?easy | medium | hard
tagstextJSON array of tags
isFavoritebooleanDefault: false
lastPlannedtext?ISO date last used

meal_plan_cycles

ColumnTypeNotes
idtext (PK)ULID
startDatetextFirst Monday (ISO date)
endDatetextLast Sunday (ISO date)
statustextactive | completed | archived
notestext?Cycle notes

meal_plans

ColumnTypeNotes
idtext (PK)ULID
cycleIdtext (FK)→ meal_plan_cycles
weekStarttext (unique)Monday ISO date
statustextdraft | pending_review | approved
generatedAttext?When agent generated
approvedAttext?When user approved
notestext?Week notes

meal_plan_entries

ColumnTypeNotes
idtext (PK)ULID
mealPlanIdtext (FK)→ meal_plans
dayOfWeekinteger0=Sun, 1=Mon, ..., 6=Sat
mealTypetextbreakfast | lunch | dinner | snack
mealIdtext? (FK)→ meals (nullable for custom)
customNametext?For custom meal entries
sortOrderintegerDisplay order
notestext?Per-entry notes

Ingredients & Shopping

ingredients

ColumnTypeNotes
idtext (PK)ULID
nametext (unique)Ingredient name
categorytext?produce | dairy | meat | seafood | pantry | frozen | bakery | beverages | condiments | spices | other
isStaplebooleanExcluded from shopping lists when true
defaultUnittext?Default measurement unit

meal_ingredients

ColumnTypeNotes
idtext (PK)ULID
mealIdtext (FK)→ meals
ingredientIdtext (FK)→ ingredients
quantityreal?Amount needed
unittext?Unit of measurement
notestext?Special notes

shopping_lists

ColumnTypeNotes
idtext (PK)ULID
mealPlanIdtext (FK)→ meal_plans

shopping_list_items

ColumnTypeNotes
idtext (PK)ULID
shoppingListIdtext (FK)→ shopping_lists
ingredientIdtext? (FK)→ ingredients
customNametext?For custom items
quantityreal?Amount to buy
unittext?Unit
isCheckedbooleanChecked off
aisleCategorytext?Store aisle category

Other Tables

household_members

ColumnTypeNotes
idtext (PK)ULID
nametextMember name
dietaryRestrictionstextJSON array (e.g. ["gluten-free"])
preferencestextJSON: {"likes": [...], "dislikes": [...]}
isActivebooleanActive member

chat_messages

ColumnTypeNotes
idtext (PK)ULID
roletextuser | assistant | system
contenttextMessage content
contextWeektext?ISO date of relevant week
isProcessedbooleanRead by agent

push_subscriptions

ColumnTypeNotes
idtext (PK)ULID
endpointtext (unique)Push service URL
p256dhtextEncryption key
authtextAuth token
deviceNametext?iPad, iPhone, Browser
isActivebooleanActive subscription

app_settings

ColumnTypeNotes
keytext (PK)Setting key
valuetextSetting value

Adding New Modules

Croft is designed to be modular. To add a new assistant module (e.g., chores, fitness):

  1. Define schema — Add tables in packages/db/src/schema.ts and generate a migration with drizzle-kit generate
  2. Add shared types — Create types in packages/shared/src/types/ and export from the index
  3. Build UI routes — Add page routes in apps/web/app/routes/ with loaders for D1 data
  4. Add API routes — Create resource routes for any UI actions and agent endpoints
  5. Create agent scripts — Add CLI scripts in agent/src/cli/ and API client methods in agent/src/services/croft-api.ts
  6. Register routes — Add to apps/web/app/routes.ts and navigation in root.tsx

D1 Caveats

  • No cascade deletes — D1 has a PRAGMA bug with onDelete: 'cascade' in Drizzle. Handle cascading deletes in application code.
  • Text-based dates — D1 uses SQLite; store dates as ISO text strings, not native date types.
  • ULIDs — All primary keys use ULIDs (time-sortable, no sequences needed).

CLI Reference

Prerequisites

Node 24 is required (managed via nvm). An .nvmrc file is in the repo root.

nvm use    # Picks up .nvmrc automatically

Development

# Start all packages in dev mode (uses Turborepo)
pnpm dev

# Run only the web app
pnpm --filter @croft/web dev

# Preview a production build locally
pnpm --filter @croft/web preview

Building

# Build all packages (via Turborepo)
pnpm build

# Build only the web app
pnpm --filter @croft/web build

# Type-check all packages
pnpm typecheck

# Lint all packages
pnpm lint

Database

# Generate a migration after changing schema files
pnpm --filter @croft/db db:generate

# Apply migrations to local D1
pnpm --filter @croft/db db:migrate:local

# Apply migrations to remote (production) D1
pnpm --filter @croft/db db:migrate:remote

# Seed the local database
pnpm --filter @croft/db seed

Deployment

# Deploy the web app to Cloudflare Workers
pnpm --filter @croft/web deploy

# Or manually from the web app directory
cd apps/web && wrangler deploy

# Generate Cloudflare Workers types
pnpm --filter @croft/web cf-typegen

Wrangler

Wrangler commands should be run from apps/web/ where wrangler.jsonc is located.

# Manage secrets (production environment variables)
cd apps/web && wrangler secret put <SECRET_NAME>

# Open the Wrangler dashboard
cd apps/web && wrangler dashboard

# Tail production logs
cd apps/web && wrangler tail

Common Workflows

# Full workflow: schema change → migration → apply
pnpm --filter @croft/db db:generate
pnpm --filter @croft/db db:migrate:local
# Test locally, then deploy:
pnpm --filter @croft/db db:migrate:remote
pnpm --filter @croft/web deploy

Local Development

Vite SSR Gotcha

Workspace packages (@croft/db, @croft/shared) need resolve.alias entries in vite.config.ts pointing to source .ts files. The SSR build can't resolve subpath exports from workspace packages otherwise.