- Python 46.8%
- JavaScript 45%
- CSS 6.8%
- Dockerfile 1%
- HTML 0.2%
- Other 0.2%
Admins: - Add is_admin to users (idempotent SQLite migration + startup bootstrap that promotes the first user; first registrant on a fresh DB also bootstraps). - /admin/users to list/create users and promote/demote (guards last admin). - Admins can set any user's reaction on a recipe (reaction endpoint user_id). Search by who likes/dislikes: - GET /recipes gains liked_by (all must favorite), not_disliked_by (exclude), and disliked_by filters; UI "Who likes it" people picker. Cookbooks: - Per-user private cookbooks (cookbooks + cookbook_recipes tables) with manual entries and/or smart tag rules (any/all) resolved at query time. - CRUD + add/remove recipe endpoints; Cookbooks list/detail pages and an "Add to cookbook" control on recipes. Also: shared RecipeCard component, /users directory for pickers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| frontend | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.override.yml | ||
| docker-compose.yml | ||
| README.md | ||
🍳 Recipe Manager
A self-hosted recipe manager: scan recipes from photos, store them in a local SQLite database, scale ingredients by servings, organize with tags, and push shopping lists to Home Assistant.
- Backend — Python / FastAPI + SQLAlchemy + SQLite (
backend/) - Frontend — React + Vite single-page app (
frontend/)
Features
- 📷 Scan a recipe from a photo — pluggable OCR: uses Claude vision (best accuracy) when an API key is set, otherwise falls back to local Tesseract OCR with heuristic parsing.
- 🗂️ Source tracking — note where a recipe came from (family, cookbook, website, …) and the specific name (e.g. "Grandma", "Joy of Cooking").
- 🔢 Servings & scaling — set base servings; ingredient quantities scale up
or down on the fly, with friendly fractions (
2 1/4 cups). - 🛒 Export to Home Assistant — push the (scaled) ingredient list to a to-do / shopping-list entity via the HA REST API. Also a one-click "copy list".
- 👥 Multiple users — register / log in (JWT). Recipes are shared: everyone can see and cook every recipe, but only the user who added one can edit or delete it. Each user has their own Home Assistant connection settings.
- ❤️ Favorites & dislikes — each user can favorite recipes (with a Favorites filter) or mark ones they won't eat; a recipe shows who's a fan and who isn't.
- 🧑🤝🧑 Cook-for-the-group search — find recipes that a chosen set of people all like and that nobody in another set dislikes (e.g. "a dinner Garnet, Malea, and Wen all like").
- 📒 Cookbooks — each user can create any number of named cookbooks and add recipes by hand and/or via smart tag rules (auto-include everything matching the tags, "any" or "all"). Cookbooks are private to their owner.
- 🛡️ Admins — the first account is an admin and can create other users, promote/demote admins, and set any user's like/dislike on a recipe.
- 🖼️ Photo uploads — attach multiple photos to each recipe.
- 🏷️ Tags / categories — free-form keywords (breakfast, dessert, sandwich…) with click-to-filter on the recipe list.
Quick start
1. Backend
cd backend
# Use the existing project venv (../.venv) or create one:
# python3 -m venv ../.venv
../.venv/bin/python -m pip install -r requirements.txt
cp .env.example .env # a SECRET_KEY is generated for you on first run if unset
# (optional) set ANTHROPIC_API_KEY in .env to enable the Claude scanner
./run.sh # serves on http://localhost:8000 (docs at /docs)
The SQLite database (recipes.db) and uploads/ directory are created
automatically on first run.
2. Frontend
cd frontend
npm install
npm run dev # serves on http://localhost:5173
Open http://localhost:5173, create an account, and start adding recipes.
In dev, Vite proxies API paths to the backend on :8000, so there's nothing
else to configure.
Recipe scanning backends
The scanner is chosen by SCAN_BACKEND in backend/.env:
| Value | Behavior |
|---|---|
auto |
Claude if ANTHROPIC_API_KEY is set, otherwise Tesseract |
claude |
Force the Claude vision model (SCAN_MODEL, default Opus 4.8) |
tesseract |
Force local OCR |
- Claude: just set
ANTHROPIC_API_KEY. Returns fully structured fields (title, ingredients split into qty/unit/name, instructions, servings, tags). - Tesseract: install the system binary (
sudo pacman -S tesseract tesseract-data-eng,apt install tesseract-ocr, orbrew install tesseract). Raw text is parsed heuristically — expect to tidy up fields.
If neither is available, scanning returns a clear error and you can still enter
recipes by hand. Check GET /api/health to see which backends are live.
Home Assistant export
In Settings, enter:
- Base URL — e.g.
http://homeassistant.local:8123 - Token — a long-lived access token (HA profile → Long-Lived Access Tokens)
- Entity — your list entity, e.g.
todo.shopping_list
Then on any recipe, pick the servings and click → Home Assistant. Each
ingredient is added via the todo.add_item service.
Docker
Both services are containerized. The frontend image runs nginx, which serves
the built SPA and reverse-proxies /api and /uploads to the backend — so the
whole app is reached on a single port.
cp .env.example .env # set SECRET_KEY (and ANTHROPIC_API_KEY if wanted)
docker compose up --build # UI on http://localhost:8080
- The backend image bundles Tesseract (with English data), so the offline
scanner works out of the box; set
ANTHROPIC_API_KEYto use Claude instead. - SQLite DB and uploaded photos persist in the
recipe-datanamed volume (mounted at/data). FRONTEND_PORTcontrols the published port (default8080).
Deploy (Forgejo Actions + Portainer)
.forgejo/workflows/build-deploy.yml builds and pushes both images to
git.thenymans.com on every push to main, then redeploys a Portainer stack,
pinning the images to the commit SHA (IMAGE_TAG). It reuses the env vars
already set on the Portainer stack, so secrets never need to be mirrored into CI.
Images are published as:
git.thenymans.com/brian/recipie-backend:{latest, <sha>}
git.thenymans.com/brian/recipie-frontend:{latest, <sha>}
If your repo owner is not
brian, changeNAMESPACEin the workflow andIMAGE_NAMESPACEindocker-compose.ymlto match.
Required secrets on the Forgejo repo:
| Secret | Purpose |
|---|---|
REGISTRY_USER |
registry login user |
REGISTRY_PASSWORD |
registry login token/password |
PORTAINER_TOKEN |
Portainer API key |
PORTAINER_STACK_ID |
id of the stack to redeploy |
PORTAINER_ENDPOINT_ID |
Portainer environment/endpoint id |
On the Portainer stack, set the runtime env (SECRET_KEY, CORS_ORIGINS,
ANTHROPIC_API_KEY, FRONTEND_PORT, …). The workflow only overrides
IMAGE_TAG; everything else is preserved across deploys.
Project layout
backend/
app/
main.py FastAPI app + static uploads mount
models.py SQLAlchemy models (User, Recipe, Ingredient, Photo, Tag)
schemas.py Pydantic request/response models
security.py PBKDF2 password hashing + JWT
scaling.py servings scaling with fraction formatting
crud.py persistence + serialization helpers
scanning/ pluggable Claude / Tesseract scanners + parser
routers/ auth, recipes, scan, tags, export
frontend/
src/
api.js fetch client
auth.jsx auth context (token in localStorage)
pages/ Login, Recipes, RecipeDetail, RecipeForm, Settings
Notes
- Tables are created automatically via
Base.metadata.create_all. For schema changes over time, introduce Alembic migrations. - Passwords are hashed with PBKDF2-HMAC-SHA256 (stdlib, no native deps).
- Set a strong
SECRET_KEYand serve over HTTPS before exposing this beyond your LAN.