Rezeptbuch — Phase 1: Scaling to a Membership Service
Drafted 2026-10-23 · Deployment target: alpine-server (167.235.27.67, Hetzner) · App URL: https://recipebook.unrivaled8.ch This plan extends the original scope in
PLAN.md(single-user app) into a multi-user, publicly reachable service.
1. Where we are today
App — Rezeptbuch, /home/agent/workspace/recipe-app: FastAPI + SQLite (data/recipe.db) +
vanilla HTML/JS (no build step), runs on port 8006. Single user, no authentication of any
kind — every API route is open.
Data inventory (as of 2026-10-23):
| Store | Contents |
|---|---|
recipe.db (app) |
3 recipes, 109 canonical ingredients, 1,182 cached Migros products, 10 imported orders (612 line items), 2 wish lists, 1 shopping list (8 items), 20 steps, 21 sync jobs |
Skill DB ~/.hermes/skills/openclaw-imports/migros-grocery/data/migros.db |
73 orders / 3,860 line items, 871 products, 674 price-history entries — only 10 orders imported so far |
Migros integration — it is not an API. There is no official public Migros API. Our "API" is
headless Chromium (Playwright) + Bright Data residential proxy (engine=brd):
scripts/migros_cart.py— login via storage state, add-to-basket, quantity, basket readscripts/sync_orders.py— discovers + scrapes new orders → JSON → app imports intomigros_orders- Session state (
migros_state.json), token cache, uid map — all bound to one account:jbachmann91@gmail.com
This works well for one user on one box. It is heavy (a full browser per operation) and fragile (site changes break selectors), so it must not run per-member.
2. Phase 1 goal
A deployed, auth-protected, multi-user Rezeptbuch on alpine-server:
- URL:
https://recipebook.unrivaled8.ch— closed beta, members join via invite code - Owner account:
jbachmann91@gmail.com— receives all existing data: the 3 recipes, ingredient catalog, wish lists, and the complete order history (all 73 orders incl. the 63 not yet imported) - Members: own private workspace (recipes, scaling, shopping lists); shared read-only product/ingredient catalog; no Migros automation in Phase 1
- Owner-only Migros automation continues server-side through a job queue
3. Architecture decisions
- Stack stays FastAPI + SQLite (WAL mode,
busy_timeout). Phase-1 volume (≤ ~50 members) is well within SQLite's comfort zone; SQLAlchemy makes the Postgres migration a Phase-2 option. - Auth: email + password (argon2 hashes), server-side sessions (HttpOnly, Secure,
SameSite=Lax), 30-day expiry with rotation.
POST /api/auth/register|login|logout,GET /api/auth/me,POST /api/auth/change-password. - Registration = invite codes only (
invitestable: code, created_by, used_by, expires_at). Public sign-up stays closed. - Data ownership:
user_idcolumn on all user-facing tables (recipes, ingredients, shopping_lists/items, wish_lists/items, migros_orders(+items), settings, uploads). Every query scoped by the session user; owner =user_id 1. Ingredient catalog shared read-only, per-member Migros links. - Roles:
admin(owner) /member. Admin-only:/api/admin/*(invite generation, user list, Migros sync trigger). - Migros worker on the server: chromium + Bright Data creds installed on alpine-server; background worker (jobs table + lock file); rate limit (max 1 order-sync / 6 h); results to admin UI. Members can never trigger it.
- Backups: nightly
sqlite3 .backup+ uploads tar → rsync offsite to the Hermes box, 7-day retention; documented restore drill (< 30 min).
4. Work breakdown
| # | Work package | Est. |
|---|---|---|
| W1 | Multi-user foundation: User/Session/Invite models, auth endpoints, UI login + user menu, query scoping | 1.5 d |
| W2 | Data ownership migration: user_id columns + backfill; seed owner jbachmann91@gmail.com; one-off import of the remaining 63 orders from the skill DB |
1.0 d |
| W3 | alpine-server deployment: rsync → /opt/rezeptbuch, venv, systemd unit, nginx vhost, DNS, TLS |
1.0 d |
| W4 | Migros worker on server: chromium deps, Bright Data creds, job queue, admin trigger, end-to-end dry-run | 1.0 d |
| W5 | Hardening: security headers, login rate limit, upload caps, backups + restore drill, ops notes | 0.5 d |
| — | Buffer | 0.5 d |
| Total | ≈ 5–6 working days |
5. Membership model
- Join: invite code → email + password in our app. No Migros credentials are ever collected — the member's Migros account is never touched (this stays the Playwright constraint).
- Members can: CRUD recipes, scale/convert quantities, shopping lists, wish lists, browse the shared product catalog with Migros candidates/prices.
- Limits: upload storage cap, no admin, no automation triggers.
- Recipes are private by default; a share/export feature is Phase 2.
6. Risks & mitigations
| Risk | Mitigation |
|---|---|
| Playwright fragility / site changes | Bright Data proxy already in use; owner-only; rate limits; manual CSV import fallback |
| Bright Data metered cost | 6-h min interval per sync; monthly GB review |
| SQLite concurrency | WAL + busy_timeout; sufficient for Phase 1; Postgres path documented |
| alpine disk at 81% | cleanup + disk monitoring before deploy; upload quotas |
| Single-box SPOF | nightly offsite backups; tested restore |
| migros.ch ToS on automation | keep to the owner's personal account, low frequency, human-like pacing (as today); review before any Phase-2 expansion |
7. Out of scope (Phase 2+)
Open registration · payments/subscriptions (Stripe) · per-member Migros cart automation (browser-extension approach discussed) · mobile app · Postgres migration · HA/redundancy · public recipe sharing.
8. Definition of done
https://recipebook.unrivaled8.chlive with valid TLS.jbachmann91@gmail.comlogs in and sees all 73 orders + 3 recipes + full catalog.- An invited member can register and CRUD their own data — and cannot see anyone else's.
- Order sync + cart dry-run succeed on the server.
- Nightly backups exist and a restore has been drilled once.