Introduces Family as the tenant boundary so recipes and cookbooks can be scoped per household instead of every user seeing everything. Adds a centralized access filter, an invite/membership UI, a first-login prompt to create a family, and locks down the previously unauthenticated backup routes to admin only. - Family and FamilyMember models with OWNER/MEMBER roles; familyId on Recipe and Cookbook (ON DELETE SET NULL so deleting a family orphans content rather than destroying it). - access.service.ts composes a single WhereInput covering owner, family, PUBLIC visibility, and direct share; admins short-circuit to full access. - recipes/cookbooks routes now require auth, strip client-supplied userId/familyId on create, and gate mutations with canMutate checks. Auto-filter helpers scoped to the same family to prevent cross-tenant leakage via shared tag names. - families.routes.ts exposes list/create/get/rename/delete plus add/remove member, with last-owner protection on removal. - FamilyGate component blocks the authenticated UI with a modal if the user has zero memberships, prompting them to create their first family; Family page provides ongoing management. - backup.routes.ts now requires admin; it had no auth at all before. - Bumps version to 2026.04.008 and documents the monotonic PPP counter in CLAUDE.md. Migration SQL is generated locally but not tracked (per existing .gitignore); apply 20260416010000_add_family_tenant to prod during deploy. Run backfill-family-tenant.ts once post-migration to assign existing content to a default owner's family. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Basil is a full-stack recipe manager built with TypeScript. It features a REST API backend, React web frontend, and PostgreSQL database. The application supports importing recipes from URLs via schema.org markup parsing, local/S3 image storage, and comprehensive recipe management (CRUD operations, search, tagging).
Architecture
Monorepo Structure:
packages/shared/- Shared TypeScript types and interfaces used across all packagespackages/api/- Express.js REST API server with Prisma ORMpackages/web/- React + Vite web application- Future:
packages/mobile/- React Native mobile apps
Key Technologies:
- Backend: Node.js, TypeScript, Express, Prisma ORM, PostgreSQL
- Frontend: React, TypeScript, Vite, React Router
- Infrastructure: Docker, Docker Compose, nginx
- Recipe Scraping: Cheerio for HTML parsing, Schema.org Recipe markup support
Database Schema:
Recipe- Main recipe entity with metadata (prep/cook time, servings, ratings)Ingredient- Recipe ingredients with amounts, units, and orderingInstruction- Step-by-step instructions with optional imagesRecipeImage- Multiple images per recipeTag/RecipeTag- Many-to-many tagging system
Storage Architecture:
- Primary: Local filesystem storage (
./uploads) - Optional: S3 storage (configurable via environment variables)
- Storage service implements strategy pattern for easy switching
Development Commands
# Install all dependencies (root + all packages)
npm install
# Development mode (starts all packages with hot reload)
npm run dev
# Build all packages
npm run build
# Lint all packages
npm run lint
# Testing
npm test # Run all unit tests
npm run test:e2e # Run E2E tests with Playwright
npm run test:e2e:ui # Run E2E tests with Playwright UI
# Docker commands
npm run docker:up # Start all services (PostgreSQL, API, web)
npm run docker:down # Stop all services
npm run docker:build # Rebuild Docker images
Backend-Specific Commands
cd packages/api
# Development with auto-reload
npm run dev
# Database migrations
npm run prisma:migrate # Create and apply migration
npm run prisma:generate # Generate Prisma client
npm run prisma:studio # Open Prisma Studio GUI
# Build for production
npm run build
npm start
Frontend-Specific Commands
cd packages/web
# Development server
npm run dev
# Production build
npm run build
npm run preview # Preview production build locally
Configuration
Environment Variables (packages/api/.env):
PORT=3001
NODE_ENV=development
DATABASE_URL=postgresql://basil:basil@localhost:5432/basil?schema=public
STORAGE_TYPE=local # or 's3'
LOCAL_STORAGE_PATH=./uploads
BACKUP_PATH=./backups
CORS_ORIGIN=http://localhost:5173
For S3 storage, add:
S3_BUCKET=basil-recipes
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=your-key
S3_SECRET_ACCESS_KEY=your-secret
For remote PostgreSQL database, update:
DATABASE_URL=postgresql://username:password@remote-host:5432/basil?schema=public
Key Features
Recipe Import from URL
POST /api/recipes/import- Scrapes recipe from URL using schema.org markup- Extracts: title, description, ingredients, instructions, times, images
- Handles JSON-LD structured data (primary) with fallback to manual parsing
- Downloads and stores images locally or on S3
Recipe Management
- Full CRUD operations on recipes
- Pagination and search (by title, description)
- Filtering by cuisine, category
- Image upload with multiple images per recipe
- Tagging system for organization
Storage Service
- Abstracted storage interface in
packages/api/src/services/storage.service.ts - Local storage: Saves to filesystem with timestamped filenames
- S3 storage: Placeholder for AWS SDK implementation
- Easy to extend for other storage providers
Backup & Restore
- Complete data backup to single ZIP file including database and uploaded files
- Backup service in
packages/api/src/services/backup.service.ts - REST API for creating, listing, downloading, and restoring backups
- Automatic backup of all recipes, cookbooks, tags, and relationships
- Configurable backup storage location via
BACKUP_PATHenvironment variable
Adding New Features
Adding a New API Endpoint
- Add route handler in
packages/api/src/routes/*.routes.ts - Update shared types in
packages/shared/src/types.tsif needed - Rebuild shared package:
cd packages/shared && npm run build - Use Prisma client for database operations
Adding a New Frontend Page
- Create component in
packages/web/src/pages/ - Add route in
packages/web/src/App.tsx - Create API service methods in
packages/web/src/services/api.ts - Import and use shared types from
@basil/shared
Database Schema Changes
- Edit
packages/api/prisma/schema.prisma - Run
npm run prisma:migrateto create migration - Run
npm run prisma:generateto update Prisma client - Update TypeScript types in
packages/shared/src/types.tsto match
Docker Deployment
The project includes full Docker support for production deployment:
docker-compose up -d
This starts:
- PostgreSQL database (port 5432)
- API server (port 3001)
- Web frontend served via nginx (port 5173)
Persistent volumes:
postgres_data- Database storageuploads_data- Uploaded imagesbackups_data- Backup files
Using a Remote Database
To use a remote PostgreSQL database instead of the local Docker container:
- Set the
DATABASE_URLenvironment variable to point to your remote database - Update
docker-compose.ymlto pass the environment variable or create a.envfile in the root - Optionally, remove or comment out the
postgresservice and its dependency indocker-compose.yml
Example .env file in project root:
DATABASE_URL=postgresql://username:password@remote-host:5432/basil?schema=public
The docker-compose.yml is configured to use ${DATABASE_URL:-default} which will use the environment variable if set, or fall back to the local postgres container.
API Reference
Recipes:
GET /api/recipes- List all recipes (supports pagination, search, filters)GET /api/recipes/:id- Get single recipe with all relationsPOST /api/recipes- Create new recipePUT /api/recipes/:id- Update recipeDELETE /api/recipes/:id- Delete recipe and associated imagesPOST /api/recipes/:id/images- Upload image for recipePOST /api/recipes/import- Import recipe from URL
Query Parameters:
page,limit- Paginationsearch- Search in title/descriptioncuisine,category- Filter by cuisine or category
Backups:
POST /api/backup- Create a new backup (returns backup metadata)GET /api/backup- List all available backupsGET /api/backup/:filename- Download a specific backup filePOST /api/backup/restore- Restore from backup (accepts file upload or existing filename)DELETE /api/backup/:filename- Delete a backup file
Important Implementation Details
Prisma Relations
- All related entities (ingredients, instructions, images, tags) use cascade delete
- Ingredients and instructions maintain ordering via
orderandstepfields - Tags use many-to-many relationship via
RecipeTagjoin table
Recipe Scraping
- Primary: Parses JSON-LD
application/ld+jsonscripts for Schema.org Recipe data - Fallback: Extracts basic info from HTML meta tags and headings
- Handles ISO 8601 duration format (PT30M, PT1H30M) for cook times
- Downloads images asynchronously and stores them locally
Frontend Routing
- Uses React Router v6 for client-side routing
- Vite proxy forwards
/apiand/uploadsrequests to backend during development - Production uses nginx reverse proxy to backend API
TypeScript Workspace
- Root
package.jsondefines npm workspaces - Packages can reference each other (e.g.,
@basil/shared) - Must rebuild shared package when types change for other packages to see updates
CI/CD and Deployment
Basil includes a complete CI/CD pipeline with Gitea Actions for automated testing, building, and deployment.
Quick Start:
- See CI/CD Setup Guide for full documentation
- See Deployment Quick Start for quick reference
Pipeline Overview:
- Test Stage: Runs unit tests (Vitest) and E2E tests (Playwright)
- Build Stage: Builds Docker images for API and Web (main branch only)
- Deploy Stage: Pushes images to registry and triggers webhook deployment
Deployment Options:
- Automatic: Push to main branch triggers full CI/CD pipeline
- Manual: Run
./scripts/manual-deploy.shfor interactive deployment - Webhook: Systemd service listens for deployment triggers
Key Files:
.gitea/workflows/ci-cd.yml- Main CI/CD workflowscripts/deploy.sh- Deployment scriptscripts/webhook-receiver.sh- Webhook server.env.deploy.example- Deployment configuration template
Required Secrets (Gitea):
DOCKER_USERNAME- Docker Hub usernameDOCKER_PASSWORD- Docker Hub access tokenDEPLOY_WEBHOOK_URL- Webhook endpoint for deployments
Version Management
IMPORTANT: Increment the version with every production deployment.
Version Format
Basil uses calendar versioning with the format: YYYY.MM.PPP
YYYY- Four-digit year (e.g., 2026)MM- Two-digit month with zero-padding (e.g., 01 for January, 12 for December)PPP- Three-digit patch number with zero-padding that increases with every deployment. Does not reset at month boundaries — it is a monotonically increasing counter across the lifetime of the project.
Examples
2026.01.006- Sixth deployment (in January 2026)2026.04.007- Seventh deployment (in April 2026 — patch continues from previous month, does not reset)2026.04.008- Eighth deployment (still in April 2026)2026.05.009- Ninth deployment (in May 2026 — patch continues, does not reset)
Version Update Process
When deploying to production:
-
Update version files:
# Update both version files with new version # packages/api/src/version.ts # packages/web/src/version.ts export const APP_VERSION = '2026.01.002'; -
Commit the version bump:
git add packages/api/src/version.ts packages/web/src/version.ts git commit -m "chore: bump version to 2026.01.002" git push origin main -
Create Git tag and release:
# Tag should match version with 'v' prefix git tag v2026.01.002 git push origin v2026.01.002 # Or use Gitea MCP to create tag and release -
Document in release notes:
- Summarize changes since last version
- List bug fixes, features, and breaking changes
- Reference related pull requests or issues
Version Display
The current version is displayed in:
- API:
GET /api/versionendpoint returns{ version: '2026.01.002' } - Web: Footer or about section shows current version
- Both packages export
APP_VERSIONconstant for internal use
UI Design System - Thumbnail Cards
Responsive Column Layout System
All recipe and cookbook thumbnail displays support a responsive column system (3, 5, 7, or 9 columns) with column-specific styling for optimal readability at different densities.
Column-Responsive Font Sizes:
- Column 3 (Largest cards): Title 0.95rem, Description 0.8rem (2 lines), Meta 0.75rem
- Column 5 (Medium cards): Title 0.85rem, Description 0.75rem (2 lines), Meta 0.7rem
- Column 7 (Compact): Title 0.75rem, Description hidden, Meta 0.6rem
- Column 9 (Most compact): Title 0.75rem, Description hidden, Meta 0.6rem
Implementation Pattern:
- Add
gridClassName = \recipes-grid columns-${columnCount}`or`cookbooks-grid columns-${columnCount}`` - Apply className to grid container:
<div className={gridClassName} style={gridStyle}> - Use column-specific CSS selectors:
.columns-3 .recipe-info h3 { font-size: 0.95rem; }
Recipe Thumbnail Display Locations
All locations use square aspect ratio (1:1) cards with 60% image height.
-
Recipe List Page (
packages/web/src/pages/RecipeList.tsx)- Class:
recipe-grid-enhanced columns-{3|5|7|9} - CSS:
packages/web/src/styles/RecipeList.css - Features: Main recipe browsing with pagination, search, filtering
- Displays: Image, title, description, time, rating
- Status: ✅ Responsive column styling applied
- Class:
-
Cookbooks Page - Recent Recipes (
packages/web/src/pages/Cookbooks.tsx)- Class:
recipes-grid columns-{3|5|7|9} - CSS:
packages/web/src/styles/Cookbooks.css - Features: Shows 6 most recent recipes below cookbook list
- Displays: Image, title, description, time, rating
- Status: ✅ Responsive column styling applied
- Class:
-
Cookbook Detail - Recipes Section (
packages/web/src/pages/CookbookDetail.tsx)- Class:
recipes-grid columns-{3|5|7|9} - CSS:
packages/web/src/styles/CookbookDetail.css - Features: Paginated recipes within a cookbook, with remove button
- Displays: Image, title, description, time, rating, remove button
- Status: ✅ Responsive column styling applied
- Class:
-
Add Meal Modal - Recipe Selection (
packages/web/src/components/meal-planner/AddMealModal.tsx)- Class:
recipe-listwithrecipe-item - CSS:
packages/web/src/styles/AddMealModal.css - Features: Selectable recipe list for adding to meal plan
- Displays: Small thumbnail, title, description
- Status: ⚠️ Needs responsive column styling review
- Class:
-
Meal Card Component (
packages/web/src/components/meal-planner/MealCard.tsx)- Class:
meal-cardwithmeal-card-image - CSS:
packages/web/src/styles/MealCard.css - Features: Recipe thumbnail in meal planner (compact & full views)
- Displays: Recipe image as part of meal display
- Status: ⚠️ Different use case - calendar/list view, not grid-based
- Class:
Cookbook Thumbnail Display Locations
All locations use square aspect ratio (1:1) cards with 50% image height.
-
Cookbooks Page - Main Grid (
packages/web/src/pages/Cookbooks.tsx)- Class:
cookbooks-grid - CSS:
packages/web/src/styles/Cookbooks.css - Features: Main cookbook browsing with pagination
- Displays: Cover image, name, recipe count, cookbook count
- Status: ✅ Already has compact styling (description/tags hidden)
- Note: Could benefit from column-responsive font sizes
- Class:
-
Cookbook Detail - Nested Cookbooks (
packages/web/src/pages/CookbookDetail.tsx)- Class:
cookbooks-gridwithcookbook-card nested - CSS:
packages/web/src/styles/CookbookDetail.css - Features: Child cookbooks within parent cookbook
- Displays: Cover image, name, recipe count, cookbook count
- Status: ✅ Already has compact styling (description/tags hidden)
- Note: Could benefit from column-responsive font sizes
- Class:
Key CSS Classes
recipe-card- Individual recipe cardrecipe-grid-enhancedorrecipes-grid- Recipe grid containercookbook-card- Individual cookbook cardcookbooks-grid- Cookbook grid containercolumns-{3|5|7|9}- Dynamic column count modifier class
Styling Consistency Rules
- Image Heights: Recipes 60%, Cookbooks 50%
- Aspect Ratio: All cards are square (1:1)
- Border: 1px solid #e0e0e0 (not box-shadow)
- Border Radius: 8px
- Hover Effect:
translateY(-2px)withbox-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) - Description Display:
- Columns 3 & 5: Show 2 lines
- Columns 7 & 9: Hide completely
- Font Scaling: Larger fonts for fewer columns, smaller for more columns
- Text Truncation: Use
-webkit-line-clampwithtext-overflow: ellipsis