recipe-manager/ARCHITECTURE.md

6.8 KiB

Recipe Manager — Architecture

Last Updated: 2026-03-23
Status: Initial Design


Tech Stack

Backend

  • Runtime: Node.js 22+
  • Language: TypeScript 5+
  • Framework: Express.js (REST API)
  • Database: SQLite 3 (sql.js — pure JavaScript, sync operations)
  • Validation: Zod (schema validation)
  • Testing: Vitest (unit), Supertest (integration)

Frontend

  • Framework: React 18+
  • Build Tool: Vite
  • Routing: React Router 6
  • State: React Context + hooks (Zustand if needed)
  • Forms: React Hook Form
  • Styling: Tailwind CSS
  • Testing: Vitest + Testing Library

DevOps

  • Containerization: Docker + Docker Compose
  • Reverse Proxy: Caddy (auto-HTTPS, simple config)
  • Process Manager: PM2 (for non-Docker deployments)
  • Backup: Litestream (continuous SQLite replication)

Architecture Patterns

1. Layered Architecture

┌─────────────────────────────────────┐
│         Frontend (React)            │
│  Components → Hooks → API Client    │
└──────────────┬──────────────────────┘
               │ HTTP/JSON
┌──────────────┴──────────────────────┐
│         Backend (Express)           │
│  Routes → Services → Repositories   │
└──────────────┬──────────────────────┘
               │ SQL
┌──────────────┴──────────────────────┐
│         Database (SQLite)           │
│     recipes, tags, collections      │
└─────────────────────────────────────┘

2. API Design

RESTful endpoints:

  • GET /api/recipes — List recipes (with filters, pagination)
  • GET /api/recipes/:id — Get single recipe
  • POST /api/recipes — Create recipe
  • PUT /api/recipes/:id — Update recipe
  • DELETE /api/recipes/:id — Delete recipe
  • GET /api/tags — List tags
  • POST /api/recipes/:id/tags — Add tag to recipe

Response format:

{
  "success": true,
  "data": { ... },
  "error": null
}

3. Data Model

CREATE TABLE recipes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  description TEXT,
  ingredients TEXT NOT NULL, -- JSON array
  instructions TEXT NOT NULL, -- JSON array of steps
  source_url TEXT,
  notes TEXT,
  servings INTEGER,
  prep_time_minutes INTEGER,
  cook_time_minutes INTEGER,
  created_at INTEGER NOT NULL, -- Unix timestamp
  updated_at INTEGER NOT NULL,
  last_cooked_at INTEGER
);

CREATE TABLE tags (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE NOT NULL,
  color TEXT -- Hex color for UI
);

CREATE TABLE recipe_tags (
  recipe_id INTEGER NOT NULL,
  tag_id INTEGER NOT NULL,
  PRIMARY KEY (recipe_id, tag_id),
  FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);

CREATE INDEX idx_recipes_title ON recipes(title);
CREATE INDEX idx_recipes_created_at ON recipes(created_at DESC);
CREATE INDEX idx_recipe_tags_recipe ON recipe_tags(recipe_id);
CREATE INDEX idx_recipe_tags_tag ON recipe_tags(tag_id);

4. Frontend Structure

src/
├── components/
│   ├── RecipeCard.tsx
│   ├── RecipeForm.tsx
│   ├── RecipeDetail.tsx
│   ├── CookMode.tsx
│   └── SearchBar.tsx
├── hooks/
│   ├── useRecipes.ts
│   ├── useRecipe.ts
│   └── useTags.ts
├── services/
│   └── api.ts           -- API client
├── types/
│   └── recipe.ts        -- TypeScript interfaces
├── pages/
│   ├── HomePage.tsx
│   ├── RecipeListPage.tsx
│   ├── RecipeDetailPage.tsx
│   └── CookModePage.tsx
├── App.tsx
└── main.tsx

Key Decisions

Why sql.js?

  • Pure JavaScript (zero native dependencies, works on Node v22+)
  • Synchronous API (simpler error handling)
  • Portable (WSL2, Docker, browser-ready for future PWA)
  • See docs/ADR-001-sqlite-library-choice.md for decision rationale

Why Tailwind CSS?

  • Rapid prototyping
  • Agent-friendly (well-documented utility classes)
  • No CSS-in-JS overhead

Why Vite over Create-React-App?

  • Faster dev server (ESM-native)
  • Simpler config
  • Better TypeScript support

Why Docker Compose?

  • Consistent dev/prod environment
  • Easy to add services (reverse proxy, backup)
  • Paul already uses Docker for fintrove

Development Workflow

1. Agent Iteration Loop

# Agent reads PROJECT.md + ARCHITECTURE.md
# Agent plans next feature
# Agent implements + tests
# Agent commits with conventional commit message
# Agent updates docs/ADR-xxx.md
# Agent reports completion

2. Testing Strategy

  • Unit tests: Core business logic (recipe parsing, scaling calculations)
  • Integration tests: API endpoints
  • E2E tests: Critical user flows (create recipe → cook mode)

3. Commit Conventions

feat: add recipe scaling
fix: correct ingredient parsing
docs: update architecture decisions
test: add cook mode E2E tests
chore: bump dependencies

Deployment

Docker Compose Stack

services:
  recipe-manager:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./data:/app/data  # SQLite database
    environment:
      NODE_ENV: production
      DATABASE_PATH: /app/data/recipes.db

  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    depends_on:
      - recipe-manager

volumes:
  caddy_data:

Backup Strategy

  • Litestream: Continuous replication to S3/B2
  • Manual exports: Weekly JSON dumps for portability
  • Git LFS: Track database snapshots (optional)

Security Considerations

MVP (Single Household)

  • No authentication (behind home network or VPN)
  • HTTPS via Caddy (Let's Encrypt)
  • Input validation (prevent SQL injection via Zod)

Future (Multi-User)

  • JWT-based auth
  • Per-user recipe collections
  • Invite-only signup

Performance Goals

  • Page load: <1s on 3G connection
  • Recipe search: <100ms for 1000 recipes
  • Cook mode: Wake lock prevents screen sleep
  • Offline support: Service worker (PWA) in v1

Open Questions for Agents

  1. Recipe scraping: Puppeteer vs Playwright vs Cheerio?
  2. Image storage: Filesystem vs base64 in DB vs S3?
  3. Serving size scaling: Fractional math library needed?
  4. Export format: JSON, CSV, or standard (Recipe Schema.org)?

Agents: Add Architecture Decision Records (ADRs) in docs/ as you make significant choices.