30 KiB
0.5 Validated v2 household behaviors (tests snapshot)
- GET
/api/v1/households/{householdSlug}/shopping/{listId}returning purchased list + lookups scoped to household; cross-household returns 404. - POST
/api/v1/households/{householdSlug}/meals/{mealId}/consumedmarks a meal consumed within the household; validates timezone on providedconsumedDate; clears outstanding meal requests only within that household. - POST
/api/v1/households/{householdSlug}/shopping/current/meals/merequests a meal under the household scope; visible only within that household in GET current. - DELETE
/api/v1/households/{householdSlug}/shopping/current/meals/{mealId}unrequests the meal (scoped) and returns{ ok: true }. - POST
/api/v1/households/{householdSlug}/shopping/current/ingredientsrequests an ad‑hoc ingredient scoped to household+user; duplicates deduped per user per household. - DELETE
/api/v1/households/{householdSlug}/shopping/current/ingredientsremoves an ad‑hoc ingredient request for the current user in this household; idempotent. - POST
/api/v1/households/{householdSlug}/recipes/parse-from-urlreturns a RecipeCreate payload parsed from a URL (stateless; household auth enforced). Shape matches the body accepted byPOST /recipes. - GET
/api/v1/households/{householdSlug}/ingredients/parse?line=...parses a single ingredient line into anIngredient(scoped; JWT + membership). Attempts best-effort product matching. - Comprehensive v2 coverage exists for scoping, purchases, request/unrequest, meals CRUD/consumed, and OpenAPI security. PASS.
Backend Specification: Household Multi-Tenancy (v2)
This document has been validated against the current codebase (v1) to ensure the plan captures all required changes. It starts with a concise baseline of what exists today, then details the v2 multi-tenancy/auth refactor with concrete, file-scoped steps and acceptance criteria.
Date reviewed: 2025-11-01 (post-cutover: JWT + households live, Argon2-only, members endpoint added, v2 routers consolidated; all checks green; OpenAPI exported; persons fully removed)
Repo modules checked: main.py, api/* (v2-only; no *_v2.py files remain), users/*, households/*, meals/*, ingredients/*, recipes/*, products/*, shopping/*, common.py, db.py, settings.py, tests in tests/*. The legacy persons/* package has been deleted.
Key conventions in v1:
- Response shape uses camelCase aliases (via
ApiModelincommon.py). - RFC7807 Problem Details are returned for 400/404/422 with
application/problem+json(handlers inmain.py). - Pagination uses a
Page<T>envelope:{ items: T[], nextCursor?: string, prevCursor?: string, total: number }. - Some endpoints require a
user_idcookie; missing cookie generally yields 422 (validation), except one special-case mapping to 401 (see below).
0. Current v1 Baseline (retired)
0.1 Auth (prototype)
- Mechanism:
user_idcookie containing a Person ID. - Endpoints (
api/auth.py):- POST
/api/v1/auth/loginbody{ username: string }→ setsuser_idcookie if person exists; 404 ProblemDetails if not. - POST
/api/v1/auth/refresh→ returns the current Person (requires cookie).
- POST
- Dependencies (
api/deps.py):cookie_person: requires cookie, loads Person by id; 401 if unknown id, 422 if cookie missing (FastAPI validation).error_response: builds RFC7807 responses.
Special-case 401: Removed. v1 cookie-based auth and routes have been retired in favor of JWT-only v2.
0.2 API surface (historical)
-
Persons API removed.
- Recipes (
api/recipes.py) - GET
/api/v1/recipes→Page<Recipe>; loads ingredients per page. - GET
/api/v1/recipes/{id}→ full recipe (ingredients + createdBy). - POST
/api/v1/households/{householdSlug}/recipes/parse-from-url(auth required) → scrape/parse a recipe URL; returnsRecipeCreate(no id/createdBy); 404 if not found. - GET
/api/v1/households/{householdSlug}/ingredients/parse?line=...→ parse a single raw ingredient line (JWT + membership); matches existing products. - POST
/api/v1/recipes(auth required) → validate (≥1 ingredient), perform versioning (hide base if id≥0), set createdById, insert ingredients; setsLocationheader. - DELETE
/api/v1/recipes/{id}(auth required) → soft-delete (hide) recipe.
- Recipes (
-
Meals (
api/meals.py)- GET
/api/v1/meals/upcoming?from&to→ upcoming unconsumed meals in range; bulk loads participants, recipes, extra ingredients. - GET
/api/v1/meals/{id}→ meal with participants/recipes/extra ingredients. - POST
/api/v1/meals→ validate meal, insert participants, recipes, extra ingredients; setsLocationheader. - PUT
/api/v1/meals/{id}→ validate/update and return current state. - POST
/api/v1/meals/{id}/consumed(auth required) → marks consumed; requires timezone inconsumed_dateif provided; removes any shopping requests for the meal. - DELETE
/api/v1/meals/{id}(auth required) → soft-delete meal via shopping cleanup then update.
- GET
-
Shopping (
api/shopping.py)- GET
/api/v1/shopping/current→ aggregate of outstanding ingredient requests (no auth), requested meals, purchased items, plus lookup maps (meals,recipes,ingredients, and any referenced purchased lists). Outward storeName enum uses"home"for internal empty string. - GET
/api/v1/shopping/{listId}→ purchased shopping list + lookups; 404 if not found. - POST
/api/v1/shopping(auth required) → purchase list with items; validates invariants in repository; returns full list + lookups. Missing/invalid cookie maps to 401 (special-case). - GET
/api/v1/shopping/current/me/ingredients(auth required) → outstanding ingredient requests for me (personId from cookie). - POST
/api/v1/shopping/current/me/ingredients(auth required) → sync my ingredient requests (add missing, remove extra). Matches existing entries byidorline. - POST
/api/v1/shopping/current/meals/me(auth required) → request a meal; 404 if meal not found; prevents duplicates. - DELETE
/api/v1/shopping/current/meals/{mealId}(auth required) → unrequest a meal; 404 if meal not found.
- GET
0.3 Data model (SQLite, created by db.create())
(legacy Person table removed; users table is canonical)
- Recipe(id PK, name, link, serves, image_urls TEXT JSON, based_on_recipe FK, date_created, created_by_id FK NOT NULL → User.id, date_hidden, hidden_by_id FK → User.id)
- Ingredient(id PK, name, line, preparation, unit, quantity REAL, product_id FK, recipe_id FK, meal_id FK)
- Product(id PK, product_id UNIQUE, shop_code, link, name, quantity, unit, img_small, img_large, raw_data TEXT) + ProductTag(food_item_id, tag)
- Meal(id PK, suggested_date, consumed_date NULL, deleted_date NULL, purchase_date NULL)
- MealParticipant(meal_id, person_id, role)
- MealRecipe(meal_id, recipe_id, servings)
- ShoppingList(id PK, created_date, store_name, purchased_by_id FK → User.id)
- ShoppingListItem(id PK, ingredient_id FK, list_id FK NULL for requests, person_id FK → User.id, meal_id FK, recipe_id FK, created_date)
0.4 Validated behaviors and invariants (carried forward into v2 where applicable)
- ProblemDetails content-type returned for 400/404/422.
- Pagination:
cursoris treated leniently (invalid → start).prevCursoris computed by a DB helper;totalis non-null integer. - Recipe creation requires ≥1 ingredient; soft-deletes older version when updating.
- Meals validation (
api.meals.validate_meal):- Must have ≥1 chef, ≥1 cleanup, ≥1 consumer.
- Must have either at least one recipe or at least one extra ingredient.
- No duplicate participants by role.
- Each MealRecipe.servings must be > 0.
- POST
{id}and body.id must match for update.
- Shopping invariants (
shopping.repository):- Purchase requires
purchased_by_idand ≥1 item. - Requests: exactly one of (ingredient|meal); person required; ingredient requests must have valid ingredient id (or be inserted when syncing); meal request must not already exist; purchased meal auto-updates
purchase_dateand clears requests when all meal ingredients are covered by list items. - Outward
storeNameuseshome|woolworths|coles(maps internal empty string tohome).
- Purchase requires
- Auth cookie missing:
- Most protected endpoints → 422 from FastAPI validation.
- Special case: POST
/api/v1/shopping→ 401 via custom handler.
1. Objective (status)
v2 household-scoped API is complete and v1 has been removed from the app. The persons package and routes are deleted. Tests have been migrated to v2 equivalents or disabled when purely legacy. OpenAPI reflects JWT bearer and household scoping.
Refactor the backend from a single-tenant architecture to a robust, multi-tenant system based on "Households". This requires evolving the data model to enforce data isolation, overhauling the authentication system to support standard credential types, and introducing an invitation mechanism for household management. This plan is adapted to the existing codebase.
2. Core Concepts & Data Model
- Data Isolation: All primary resources (
recipes,meals,shopping_lists, etc.) MUST be strictly scoped to ahousehold_id. No API endpoint should ever return data from a household the authenticated user does not belong to. - User vs. Person: The existing
personstable andPersonmodel will be replaced by auserstable and aHouseholdMemberlink table. - Authentication: Replace the prototype
user_idcookie with a standard JWT-based system.
New & Modified Data Models
households(New Table):id: Primary Keyname:TEXT NOT NULLslug:TEXT NOT NULL UNIQUE
users(New Table, replacespersons):id: Primary Keyemail:TEXT NOT NULL UNIQUEdisplay_name:TEXT NOT NULLprofile_photo_url:TEXT
household_members(New Table):user_id: FK tousers.idhousehold_id: FK tohouseholds.idrole:TEXT NOT NULL(e.g., 'admin', 'member')PRIMARY KEY (user_id, household_id)
local_credentials(New Table):user_id: PK, FK tousers.idhashed_password:TEXT NOT NULL
oauth_credentials(New Table):user_id: FK tousers.idprovider:TEXT NOT NULL(e.g., 'google')provider_user_id:TEXT NOT NULLPRIMARY KEY (provider, provider_user_id)
household_invitations(New Table):id: Primary Keyhousehold_id: FK tohouseholds.idemail:TEXT NOT NULLinvited_by_user_id: FK tousers.idtoken:TEXT NOT NULL UNIQUEexpires_at:DATETIME NOT NULLstatus:TEXT NOT NULL('pending', 'accepted', 'expired')
3. API & Logic Changes
3.1. Authentication API (current)
api/auth.pyimplements JWT-based authentication with Argon2 hashing for new passwords (PBKDF2 verification fallback for legacy hashes).POST /api/v1/auth/register:{ email, password, displayName }→ creates User + LocalCredentials; returns access token; sets HttpOnly refresh cookie.POST /api/v1/auth/login:{ email, password }→ validates; returns access token; sets HttpOnly refresh cookie.POST /api/v1/auth/refresh: returns a fresh access token (reads refresh cookie).POST /api/v1/auth/logout: clears refresh cookie. After logout, subsequentPOST /api/v1/auth/refreshreturns 401.
Notes:
get_current_user(JWT) is used by protected endpoints (households, shopping purchase, etc.). Legacy cookie-based helpers have been removed from the app surface.- RFC7807 error semantics and the existing OpenAPI augmentation are preserved. The special-case 401 mapping for POST shopping is obsolete under JWT.
3.2. Household & Tenancy API (current)
api/households.py:GET /api/v1/users/me/households: (JWT) lists the user's households.POST /api/v1/households: (JWT) creates a household and adds the creator as admin.- Scoped router
/api/v1/households/{householdSlug}withGET /whoamiandGET /members(returns{ id, displayName, role }).
Household-scoped routes (implemented):
api/recipes.py:/api/v1/households/{householdSlug}/recipeslist/get/create/delete.api/meals.py:/api/v1/households/{householdSlug}/mealsupcoming/get/create/update/consumed/delete (inlined from v2).api/shopping.py:/api/v1/households/{householdSlug}/shoppingcurrent/list-by-id/purchase/request/unrequest (inlined from v2, with shared DTOs inapi/shopping_models.py).
Shopping requests parity (preserved in v2):
- Request meal:
POST /api/v1/households/{householdSlug}/shopping/current/meals/me(scoped) and unrequestDELETE /current/meals/{mealId}. - Request individual ingredient:
POST /api/v1/households/{householdSlug}/shopping/current/ingredients(scoped). Response isListIngredientItem; item appears inGET /currentunderoutstandingItems. Household isolation enforced. Duplicate requests for the same ingredient by the same user within the same household return the existing request (no duplicate rows). Covered bytests/test_shopping_request_ingredient_dedupe_v2.py. - Unrequest individual ingredient:
DELETE /api/v1/households/{householdSlug}/shopping/current/ingredients(scoped). Body{ ingredientId }. Returns{ ok: true }even if nothing was deleted.
Repositories accept household_id and filter by it across meals, recipes, and shopping domains.
Route surface lockdown:
- Legacy unscoped endpoints (e.g.,
/api/v1/meals/*,/api/v1/shopping/*) are not exposed; all data routes are under/api/v1/households/{householdSlug}/.... - Protected household routes return 401 without Authorization; with Authorization, non-existent households yield 404, and non-membership yields 403.
- Tests:
tests/test_route_surface_lockdown.pyasserts these behaviors.
3.3. Invitation API
- Add to
api/households.py:POST /api/v1/households/{householdSlug}/invitations: (Auth: JWT, household membership). Body{ email }. CreatesHouseholdInvitation, sends email.
- Add to
api/auth.py:POST /api/v1/invitations/accept: (Auth: JWT). Body{ token }. Validates token, adds user to household, and returns{ status: "accepted", household: { id, name, slug } }.
4. Actionable Implementation Steps
- [~] Database Schema and Migration:
- ✅ Schema Definition: Added new packages and tables:
userswith tablesUser,LocalCredentials,OAuthCredentials(seeusers/repository.py).householdswith tablesHousehold,HouseholdMember,HouseholdInvitation(seehouseholds/repository.py).db.create()now initializes these tables alongside existing v1 tables.
- ✅ Migration Logic: Implemented
scripts/migration_to_households.pywhich:- Connect to the database (reusing logic from
db.py). - Calls
create()for new repos to ensure tables exist. - Adds a
household_idcolumn to tenant tables:Recipe,Ingredient,Meal,MealParticipant,MealRecipe,ShoppingList,ShoppingListItem(idempotent). - Creates indices
idx_<table>_household_idfor all above tables. - Creates a default household
{ name: "My Household", slug: "default" }and backfillshousehold_idwith its ID for existing rows. - Ports
Personrows toUser(email derived as<name>@example.com) and createsHouseholdMemberlinks (roleadmin).
- Connect to the database (reusing logic from
- ✅ Schema Definition: Added new packages and tables:
Feature parity checklist (OpenAPI diffs vs master)
Completed:
- Auth endpoints (
/api/v1/auth/*) migrated to JWT with tokens/refresh cookie. - Household-scoped recipes/meals/shopping endpoints in place.
POST /api/v1/households/{householdSlug}/recipes/parse-from-urlimplemented (returnsRecipeCreate).GET /api/v1/households/{householdSlug}/ingredients/parseimplemented (returns singleIngredient, scoped).
Outstanding (tracked):
- None identified blocking parity for shopping/recipes needed by the frontend as of 2025-11-01. Re-check if any v1 product scrape/create endpoint needs re-exposure; current frontend uses household flows and parsing utilities.
-
Bootstrap Update: Modify
db.pyso that a fresh database bootstrap (db.create_schema) calls thecreate()functions for the new repositories and not the oldpersonsrepository. -
✅ Indices/Constraints: Enforced unique
Household.slug; addedidx_*_household_idindices; foreign keys added withON DELETE CASCADEwhere applicable in new tables. -
✅ Acceptance (initial): Added
tests/test_migration_households.pycovering: new tables exist,household_idcolumns exist, default household created, and data porting fromPersontoUserandHouseholdMember. Full test suite passes. -
Pending follow-ups for this step:
- (Done) Add composite indices
(household_id, id)where high-cardinality pagination will benefit (Recipe, Meal). - Extend migration to add FK constraints from tenant tables to
Household(id)where safe. - Plan and implement data backfill for cross-table references once
usersreplacepersonsin code. - Ensure fresh bootstraps include
household_idin all tenant table DDL (now updated for Ingredient, Recipe, Meal, ShoppingList, ShoppingListItem).
- (Done) Add composite indices
-
-
[✅] Implement New Authentication System:
- Implemented JWT auth (Argon2 password hashing) and removed v1 cookie auth:
api/auth.pyissues HS256 JWT access tokens and sets an HttpOnly refresh cookie.- Endpoints:
POST /api/v1/auth/register,POST /api/v1/auth/login,POST /api/v1/auth/refresh,POST /api/v1/auth/logout.
- Endpoints:
api/deps.get_current_userverifies JWT access tokens and loads theUserfrom DB.security.pyprovides a minimal JWT utility with configurable issuer/audience, secrets, and TTLs.settings.pyextended with JWT config and secrets via env.- Tests updated to expect JWT-shaped tokens and verify refresh flow.
- OpenAPI augmentation marks
/users/me/*and/households/*withbearerAuth+403.
- Notes:
- Password hashing upgraded to Argon2 via
argon2-cffi. - v1 cookie auth is removed.
- Password hashing upgraded to Argon2 via
- Acceptance: Unauthenticated requests return 401; household membership failures continue to return 403; tests pass.
- Implemented JWT auth (Argon2 password hashing) and removed v1 cookie auth:
-
[✅] Implement Household Scoping:
- ✅ Created
households/package withmodels.pyandrepository.py. - ✅ Added initial
api/households.pyrouter:GET /api/v1/users/me/households(requires bearer token) → lists memberships.POST /api/v1/households(requires bearer token) → creates household and adds current user as admin.
- ✅ Implemented
get_household_from_sluginapi/deps.py. - ✅ Refactor
main.py:- Household-scoped routers consolidated under
api/households.pyand mounted at/api/v1/households/{householdSlug}. - Recipes, meals, and shopping routers are v2-only and live in
api/recipes.py,api/meals.py, andapi/shopping.pyrespectively.
- Household-scoped routers consolidated under
- ✅ Created
-
✅ Recipes scoping:
- Consolidated into
api/recipes.pyproviding/api/v1/households/{householdSlug}/recipeswith list/get/create/delete.- Scoped repo helpers in
recipes/repository.pyand exported viarecipes/__init__.py. - Tests validate isolation across households (PASS).
- Scoped repo helpers in
- ✅ Meals: Consolidated into
api/meals.pywith:/api/v1/households/{householdSlug}/meals/upcomingfiltering byhousehold_id./api/v1/households/{householdSlug}/meals/{id}returns 404 across households.
- Consolidated into
-
POST /api/v1/households/{householdSlug}/meals/{id}/consumedmarks consumed with optionalconsumedDate(requires timezone if provided); removes outstanding meal requests; all operations scoped to household. -
POST /api/v1/households/{householdSlug}/mealscreates a meal (scoped) with Location header; reuses v1 validation (≥1 chef, ≥1 cleanup, ≥1 consumer; ≥1 recipe or ≥1 extra ingredient). -
PUT /api/v1/households/{householdSlug}/meals/{id}updates a meal (scoped), enforcing URL/body ID match and validation; returns updated state. -
DELETE /api/v1/households/{householdSlug}/meals/{id}soft-deletes a meal (scoped) after removing outstanding requests in that household.- Repository functions
find_upcoming_meals_by_date_range_scopedandfind_meal_by_id_scopedadded. Tests verify isolation and consumed behavior.
- Repository functions
-
✅ Shopping:
- Added
api/shopping_v2.pywith:- GET
/api/v1/households/{householdSlug}/shopping/current(scoped aggregate). - GET
/api/v1/households/{householdSlug}/shopping/{listId}returning purchased list + lookups scoped to household; cross-household returns 404. - POST
/api/v1/households/{householdSlug}/shoppingto purchase list items scoped to household; validates invariants and updates outstanding requests.
- GET
- Scoped helpers in
shopping/repository.pyandshopping/__init__.pyfilter byhousehold_id(find items, load list, purchased ingredients, and purchase_scoped).
- Added
-
Tests:
tests/test_shopping_household_v2.py(current isolation),tests/test_shopping_list_by_id_v2.py(list-by-id scoping),tests/test_shopping_purchase_v2.py(scoped purchase),tests/test_shopping_request_meal_v2.py(request/unrequest). PASS. -
Notes: Normalized Ingredient.preparation to allow NULLs from DB (treated as empty string) to avoid 422 in v2 responses; set purchased_by_id in scoped purchase from JWT user.
-
Tests:
tests/test_meals_write_v2.pyverifies create/update/delete flows under household scope; all v2 scoping tests PASS. -
Acceptance: The same queries as v1, when run under different household slugs and users, return isolated data sets; cross-household access yields 403. Achieved.
-
Notes:
- Migration added
household_idcolumns and indices, enabling next step to filter by household without additional schema changes. - Data access policy: API layers must delegate persistence to repository modules; no direct SQL in routers. Current status: recipes, meals, and shopping routers call into their repositories for reads/writes. Legacy
*_v2.pyfiles have been removed; canonical routers areapi/recipes.py,api/meals.py, andapi/shopping.py.
- Migration added
- [~] Implement Household & Invitation Logic:
- ✅ Households router implemented for listing and creating households.
- ✅ Invitations API:
POST /api/v1/households/{householdSlug}/invitations(JWT + membership): creates a pending invitation and returns a token.POST /api/v1/invitations/accept(JWT): validates token, adds user as member, marks invitation as accepted.- Tests:
tests/test_invitations_v2.pycover create + accept flow and membership visibility.
- ⏳ Email Delivery: Stub only. Pending adding an email sender utility/service and persistence of delivery state.
- Acceptance: Admins can invite by email; invite accept adds user to household; listing households shows membership with roles. (Met for API behavior; email sending pending.)
- [✅] Update OpenAPI Specification:
- ✅ Augmentation updated in
api/openapi.py:- Adds
bearerAuthsecurity scheme (cookieAuth removed). - Marks household routes and
/users/me/*with bearer security and adds403Problem response. - Preserves RFC7807 Problem responses and shopping storeName outward enum normalization.
- Adds
- ✅ Augmentation updated in
- ✅ New tests
tests/test_openapi_security.pyverify presence of bearerAuth and 403s. - ✅ Export script writes updated
openapi.json; re-run after adding meals v2 write endpoints to include them in the schema.
- [✅] Refactor and Test:
- Tests updated to v2 household-scoped endpoints with JWT setup. Legacy v1 API tests are skipped (
tests/test_v1.py), and the legacytests/test_main.pyhas been removed. - Full suite green under
make all-checks. OpenAPI export successful. - Next cleanup: remove
persons/package and remaining references in domain internals once users fully replace persons in models.
5. Gaps vs Current Codebase (summary)
Status summary:
- Implemented: JWT auth v2 with refresh cookie; household domains and membership; invitations; full household scoping across recipes/meals/shopping; OpenAPI augmentation with bearerAuth and 403; RFC7807 preserved; tests green.
- DTO alignment: Meals use MemberRef { id, displayName } (no Person in outward schema). MemberRef consolidated in
api/dtos.py. Shopping DTOs/mappers consolidated inapi/shopping_models.py. - DTO alignment (v2): Meals use MemberRef; Recipes include
createdByIdandcreatedBy(MemberRef); ShoppingpurchasedByis now a MemberRef on outward lists. - Security: Password hashing now uses Argon2 exclusively (argon2-cffi). No PBKDF2 fallback remains.
- New: Household members listing
GET /api/v1/households/{householdSlug}/membersreturning [{ id, displayName, role }]. - Preserved: camelCase responses,
Page<T>semantics,Locationheaders on create, shopping storeName normalization ("home"). - Codebase cleanup: v2 routers are inlined as canonical modules; legacy
*_v2.pyfiles removed. v1 cookie auth and routers are not mounted. - Current v2 recipes shape:
createdByIdandcreatedBy(MemberRef) are included. Delete now returnshiddenByIdandhiddenBy(MemberRef) for the acting user.
Remaining work (prioritized cleanup to final state):
- Remove the
persons/package and all code references across domains (recipes, meals, shopping). ReplacePersonwithusers/HouseholdMember everywhere:
- Code hotspots today:
recipes/models.py(imports Person),recipes/repository.py(FKs, hide_recipe signature),meals/models.py(participants as List[Person]),api/shopping_models.pyandshopping/models.py(ShoppingList.purchased_by typed as Person), andshopping/repository.py(FKs to Person). - Replace internal usages with
user_id/MemberRefwhere outward, and update repository DDL to stop referencingPerson. - Removed
api.deps.cookie_personandcookie_person_optional; v1 tests referencing them remain skipped and will be deleted or ported. - Ensure no API module performs direct SQL; all persistence must flow through repositories (enforced during cleanup).
- Recipes outward schema: DONE for
createdById/createdByandhiddenById/hiddenBy(MemberRef on delete). If/when we expose hide actor on other flows, keep MemberRef. - Shopping outward DTOs: DONE —
ShoppingListOut.purchasedByis a MemberRef; mapping added. Ensure clients shift todisplayName. - Delete or port legacy v1 test modules that are currently skipped (
tests/test_main.py,tests/test_v1.py). Either migrate assertions to v2 routes or remove them so the full test run has no skips. Then remove the last vestiges of v1-only helpers. - Database polish:
- Add composite indices like
(household_id, id)where pagination benefits (e.g., Recipe, Meal, ShoppingListItem). - Add explicit FK constraints from tenant tables to
Household(id)where safe. - Ensure fresh bootstraps include
household_idin table DDL (e.g.,recipes.repository.create), not just via migration.
6. Edge Cases and Error Semantics
- Unauthenticated → 401 (JWT), not 422 (replace the v1 cookie-missing 422/401 hybrid behavior).
- Not a member of household → 403 with ProblemDetails.
- Validation stays 400 with ProblemDetails (e.g., meal validation failures; recipe without ingredients; shopping invariants).
422reserved for request model validation errors.
7. Rollout & Migration Plan (updated)
- Phase A: Introduced
usersalongsidepersons; added JWT while cookie auth remained briefly for transition. - Phase B: Added households and backfilled default household; migrated routes under
/households/{householdSlug}. - Phase C (now): Removed cookie auth from app surface and inlined v2 routers; final step is to remove
personsentirely and ensure no tests are skipped.
Data migration acceptance:
- All existing data appears under a default household and is accessible to the migrated user accounts.
8. Acceptance Criteria (v2)
- Authentication: JWT bearer with register/login; refresh via HttpOnly cookie;
get_current_userused in protected routes. - Tenancy: All read/write queries filter by
household_id; cross-household access returns 403; invitation flow works. - API parity: v1 behavior preserved aside from auth/paths; ProblemDetails and pagination semantics intact; Location headers set on creates.
- OpenAPI: Security scheme is JWT; household path param present; arrays required and enums normalized; 400/404/422 standardized; 403 added on household routes.
Final-state definition (what “done” looks like):
- No legacy v1 endpoints mounted; no
*_v2.pyfiles in repo (done). - No
personspackage in codebase or responses; all tests ported from v1 and no tests are skipped. - OpenAPI reflects only JWT-secured, household-scoped endpoints.
Implementation policy updates (2025-11-01):
- Routers must use repository helpers for all persistence; direct
conn.execute(...)calls in routers are prohibited, except for controlled PRAGMA/transaction management inapi/deps.py. - Recipes create now accepts a lean body (
RecipeCreate) without internal IDs and setscreatedByIdfrom the JWT user; delete uses a repository helper to setdate_hiddenandhidden_by_idatomically. - Canonical API files are:
api/recipes.py(household-scoped recipes)api/meals.py(household-scoped meals)api/shopping.py(household-scoped shopping)api/households.py(memberships and scoped helpers)api/auth.py(JWT register/login/refresh/logout)
Outstanding cleanup:
- Remove remaining
Personfallbacks in domain repositories and delete thepersons/package once tests are migrated. Ensure no code paths depend oncookie_person. - Consider adding versioning endpoints for recipes explicitly rather than overloading POST.
- Add a temporary test helper to seed
User/HouseholdMemberrows from legacyPersonwhen needed (now available astests/user_fixtures.py). Use this to migrate tests offpersonsbefore deleting the package.