20 KiB
0. Current State (Nov 1, 2025)
All core features are migrated to multi-tenancy with path-scoped endpoints and token-based auth. The codebase no longer uses the X-Household-Slug header. Tests and type checks are fully green.
0. Current State (Nov 1, 2025)
All core features are migrated to multi-tenancy with path-scoped endpoints and token-based auth. The codebase no longer uses the X-Household-Slug header. Tests and type checks are fully green.
Status of tests and typing
- All tests pass: 27 files, 46 tests (slug-only routes; memory history fallback in non-browser envs; unauthenticated and refresh-401 guard redirects covered).
tscandvue-tscpass with no errors.
Frontend Specification: Household Multi-Tenancy (v2)
1. Objective
Transition the frontend from a single-tenant application to a multi-tenant one centered around the concept of "Households". This involves refactoring the existing authentication system, user onboarding, and data presentation to ensure all information (recipes, meals, shopping lists) is scoped to the active household.
This plan is adapted to the existing codebase, focusing on refactoring rather than starting from scratch.
2. Core Concepts
- Household: A private group of users. All data is isolated and only visible to members of that household.
- URL-based Tenancy: The active household is determined by the URL, e.g.,
/the-smiths/recipes. This makes household context explicit and shareable. - Household Switching: Users belonging to multiple households can switch between them.
- Authentication: Evolve the existing system from a simple username login to a robust one supporting email/password and Google OAuth, using JWTs.
3. User Flows & UI Requirements
3.1. Authentication & Onboarding
- Login Page (
/login):- Refactor
src/components/LoginPage.vue. - Replace the current username input with:
- Email & Password login fields.
- A "Sign in with Google" button.
- Add a link to the "Create Account" page (
/create-account).
- Refactor
- Create Account Page (
/create-account):- Create a new view
src/views/CreateAccount.vue. - UI components for:
- Email, Display Name, and Password input.
- "Sign up with Google" button.
- A link back to the "Login" page.
- Create a new view
- Post-Login Household Selection (
/welcome):- Create a new view
src/views/Welcome.vue. - After a new user logs in for the first time, they are directed here.
- The page should present two choices:
- Create a new household: A simple form with "Household Name".
- Join an existing household: This section should clearly state: "To join a household, ask an existing member to send an invitation to your email address."
- Create a new view
- Invitation Flow:
- A user receives an email with a link like
https://<app-domain>/invitations/accept?token=.... - Visiting this link while logged in should add them to the household and redirect them to that household's dashboard.
- A user receives an email with a link like
3.2. In-App Experience
- Household-Scoped URLs:
- All application routes must be nested under a household slug:
/:householdSlug/recipes,/:householdSlug/shopping, etc. - The router must be updated to handle this dynamic parameter. The
householdSlugwill be used in all API calls to fetch household-specific data.
- All application routes must be nested under a household slug:
- Household Switcher:
- Create a new component
src/components/HouseholdSwitcher.vue. - Place it in a prominent location (e.g., inside
App.vue's navigation bar). - It should list all households the current user is a member of.
- Clicking a household name should navigate the user to the dashboard of that household, e.g.,
/<new-household-slug>/dashboard.
- Create a new component
- Invite Members UI:
- Create a new view
src/views/HouseholdSettings.vue. - It should be accessible at
/:householdSlug/settings/members. - It should contain a simple form to enter an email address and a button to "Send Invitation".
- It should also list current household members.
- Create a new view
4. Actionable Implementation Steps
- [x] Refactor Authentication State & API:
- Modify
src/api/auth.ts:- Implemented JWT-based login/register using new endpoints; Authorization header injected via client provider.
loginWithPassword(email, password)andcreateAccount(email, displayName, password)now returnUserand set token.logout()clears token and cached user.
- Modify
currentUser()now performs token-only refresh (no user in response) and then loads user context via/api/v1/users/me/households.- Modify
src/composables/useAuth.ts:- Updated to use
User, addedloginWithPassword+logout, and state forhouseholds+activeHousehold. - Added
fetchHouseholds()which hits/api/v1/users/me/householdsand stores state.
- Updated to use
- Modify
- [x] Update Router for Multi-Tenancy:
- Modify
src/router/index.ts:- Added new public routes:
/create-account,/welcome, and/invitations/accept. - Removed legacy flag and flat routes: feature routes are always nested under
/:householdSlug/.... - Refactor the
beforeEachguard:- Allows public routes.
- After auth, fetches households via
useAuth().fetchHouseholds(). - If none: redirect to
/welcome. - If at root (
/): redirect to first household's/:householdSlug/mealplan. - Ensures
activeHouseholdis set when navigating within a household.
- Added new public routes:
- Modify
- Nest existing routes: Always under
/:householdSlug(legacy flat routes removed). - History behavior: Uses hash history in real browsers and memory history in tests/SSR (detected via
globalThis.location). Router tests assert slug-only mode.
- [~] Implement Onboarding and Invitation Flows:
- Build the
Welcome.vueview for creating the first household. - Build the
CreateAccount.vueview. - Build the "Accept Invitation" page (
/invitations/accept?token=...). It should take the token from the URL, call the API, and redirect on success.
- Build the
- Status: Welcome page implements create-household flow using
POST /api/v1/householdsand redirects to/:slug/mealplan. Create Account UI implemented. Invitation Accept implemented: readstokenfrom query, calls typedPOST /api/v1/invitations/accept, and redirects to the accepted household.
-
[~] Integrate Household Context into the App:
- Create
src/composables/useHousehold.ts: Implemented. ExtractshouseholdSlugfrom route and binds provider to API client. - Implement
HouseholdSwitcher.vue: Implemented minimal version and mounted inApp.vue. - Update API Services: No household header injection; Authorization header provided by token provider. Cookies are only sent for the refresh endpoint; all other requests avoid credentials. SDK/services use typed path params.
- Verified by
tests/household.header.test.ts, auth tests, and router guard tests.
- Verified by
- Create
-
[~] Implement Invitation UI:
- Build the
HouseholdSettings.vueview for inviting members and listing current members. - Status: Invite form implemented (sends email via POST
/api/v1/invitations). Members listing implemented using typed endpointGET /api/v1/households/{householdSlug}/members.
- [ ] Final Review & Cleanup:
- Remove the old
personsconcept from the frontend code. TheuserfromuseAuthis now the primary identity. - Ensure all data displays are correctly filtered by the active household by verifying the
householdSlugis passed in all API calls. - Test all user flows: new user signup, login, creating a household, joining via invitation, and switching between households.
- Remove the old
0. Current State (Nov 1, 2025)
All core features are migrated to multi-tenancy with path-scoped endpoints and token-based auth. The codebase no longer uses the X-Household-Slug header. Tests and type checks are fully green.
What exists now
- Auth
src/api/auth.ts: email/password login and register; token-only refresh incurrentUser()which then loads/api/v1/users/me/households.src/composables/useAuth.ts: managesuser,households, andactiveHousehold; exposes login/logout/createAccount andfetchHouseholds().
- Routing
src/router/index.ts: slug-only nesting under/:householdSlug/...; public routes include/create-account,/welcome, and/invitations/accept.- Guard fetches households, redirects root
/to the first household'smealplan, and uses memory history in tests (hash in browser).
- SDK/API
src/api/sdk.ts: recipes, meals, and shopping are migrated to/api/v1/households/{householdSlug}/...typed endpoints. Person endpoints removed; parse uses a temporary raw fetch endpoint until typed coverage is available.src/api/client.ts: Authorization header provider only; household header injection removed.
- Domain & UI
- Member arrays (
chefs,consumers,cleanup) normalized toMemberRef{ id, displayName }with decoders handling legacy shapes gracefully. MemberRefis exported fromsrc/domain/types.tsand used by components (per axioms).PersonList.vuerenamed toMemberList.vue; it sources from typed household members and emitsadd/remove.- Invitation Accept flow implemented; Household Settings supports sending invitations and listing members using typed endpoints.
- MyShopping: page remains as in master with editable panel backed by legacy v1 stubs (
getMyShoppingList/saveMyShoppingList) pending backend ad-hoc item endpoints.
- Member arrays (
Status of tests and typing
- All tests pass: 27 files, 46 tests.
tscandvue-tscpass with no errors.
Amendment: API Services and Typing Strategy (Updated Nov 1, 2025)
The backend OpenAPI has been updated and now exposes householdSlug as a path parameter for scoped endpoints (recipes, meals, shopping, invitations, whoami). Auth endpoints are fully typed (login/register/refresh/logout). Invitation acceptance remains a global endpoint (/api/v1/invitations/accept) with a typed operation.
Implications and actions (completed):
- Removed
X-Household-Slugand migrated to typed path parameters across recipes, meals, and shopping. currentUser()updated to token-only refresh and household loading.- Invitations: sending is typed under household scope; accept is typed globally.
- Members listing is now typed under
GET /api/v1/households/{householdSlug}/members; UI updated to displaydisplayNameandrole.
Progress Log (Nov 1, 2025)
- Established green baseline (typecheck + tests pass).
- Added auth API tests driving a minimal multitenant-ready surface.
- Implemented
loginWithPassword,logout, and stubs insrc/api/auth.tsto satisfy tests. - Refactored
useAuthto add households and activeHousehold state, plusloginWithPasswordandlogout. Tests added and passing. - Added router tests and implemented slug-only nested routes and new public routes. Placeholders for onboarding/invitations added.
- Implemented
useHousehold.ts, Authorization provider in API client, minimalHouseholdSwitcher.vue, and mounted it. Replaced header injection test with path-scoped assertion. - Implemented JWT login/register in
auth.tsand wired token to client provider.useAuthupdated with households fetching.currentUserrefactored to token-only refresh plus households load. - Router guard updated to handle public/multitenant routing and redirects.
- Added
useAuth.createAccountwith state update and tests for it; implementedCreateAccount.vuewith form and navigation. - Implemented Invitation Accept flow: added
src/api/invitations.tswith typedacceptInvitation,InvitationAccept.vuereads token and redirects to household; addedtests/invitations.api.test.ts. - Next: Implement Household Settings (invite members form), then remove legacy Person UI.
- Added a Settings link to
HouseholdSwitcher.vueto surface thehousehold-settingsroute for easier discovery. - Implemented Household Settings invite form and members list UI.
src/views/HouseholdSettings.vuenow loads members via the typed endpoint. - Router uses memory history in tests to avoid relying on
window.location. Added unauthenticated and refresh-401 guard redirect tests. - Backend updated OpenAPI and codegen has been run:
- Many endpoints are now path-scoped with
{householdSlug}(recipes, meals, shopping, invitations (create), whoami). - Auth endpoints (login/register/refresh/logout) are fully typed;
refreshreturns only{ accessToken, tokenType }.
- Many endpoints are now path-scoped with
- Completed migration to typed path parameters; header injection removed; only small raw fetch helpers remain for endpoints not yet in OpenAPI (parse only).
Refinements (Nov 1, 2025, later):
- Lint hardening: removed remaining
as anyand unsafe assertions across SDK/decoders. - decodeRecipe/decodeMeal simplified to use concrete OpenAPI shapes and defaults; legacy Person normalization removed.
- Raw parse endpoints now use small runtime guards and normalize to strict RecipeOut before decoding.
- UI polish: MemberList and EditMealPage CSS class names unified (person-* → member-*). Login page shows a Google sign-in button wired to the placeholder handler.
Detailed Tasks by File/Module
New files
src/views/CreateAccount.vue: Email, Display Name, Password; Google signup; link to login.src/views/Welcome.vue: Create first household or instructions to join via invite.src/views/HouseholdSettings.vue: Invite members by email; list members.src/components/HouseholdSwitcher.vue: Lists user households and navigates to selected household dashboard.src/composables/useHousehold.ts: ExposesactiveHouseholdSlugfrom route and small helpers.
Auth
src/api/auth.ts:- Replace
login(username: string)→login(email: string, password: string). - Add
createAccount,handleGoogleLogin,logout; updatecurrentUserto refresh JWT/session. - Store token per backend guidance; clear on
logout.
- Replace
src/composables/useAuth.ts:- Manage
user,households,activeHouseholdstate; exposelogin,register,handleGoogleLogin,logout,loadUser,setActiveHousehold.
- Manage
Router
src/router/index.ts:- Add public routes:
/create-account,/welcome,/invitations/accept. - Nest feature routes under
/:householdSlug. - Guard: after auth, fetch households; redirect root
/to first household dashboard; route/welcomeif none. - Update imperative navigations to include
{ householdSlug }via named routes.
- Add public routes:
API client
src/api/client.ts:- Uses typed
{ params: { path: { householdSlug } } }across SDK;X-Household-Slugheader provider removed. - Authorization provider remains as-is, fed by JWT token from login/refresh.
- Uses typed
SDK
src/api/sdk.ts:- Recipes, meals, shopping fully path-scoped to households.
- Newly typed endpoints (post-latest codegen):
GET /api/v1/households/{householdSlug}/shopping/current— current aggregated list.GET /api/v1/households/{householdSlug}/shopping/{list_id}— purchased list by id.POST /api/v1/households/{householdSlug}/shopping— purchase items (storeName + items).POST /api/v1/households/{householdSlug}/shopping/current/meals/me— request a meal.DELETE /api/v1/households/{householdSlug}/shopping/current/meals/{meal_id}— unrequest a meal.POST /api/v1/households/{householdSlug}/shopping/current/ingredients— request an ingredient (by id) for shopping.
- Persons and parse endpoints are still raw until they are added to the OpenAPI.
UI
- Login Page: Refactored to show email/password form; Google sign-in planned. Link to Create Account added.
- Add
HouseholdSwitcher.vueto app chrome and wire with router. - Update components that navigate using string paths to use named routes with slug.
src/views/HouseholdSettings.vue: Invite members form wired tosendInvitation(email). Members list rendered fromlistMembers()using the typed endpoint. (Completed)- Adjust pages that call SDK/API to pass
{ householdSlug }path params once client services are migrated.
Tests
- MSW handlers updated for path-scoped endpoints; Authorization header assertions retained where relevant.
- Router tests run under memory history.
- Replaced header injection test with path-scoped recipe list test.
OpenAPI & Typing Considerations (Updated)
- Avoid
any/unknownin app code; keep all API calls typed viaopenapi-fetch. - Household scoping: use typed path params (
{ params: { path: { householdSlug } } }); do not mutate path strings. - Auth refresh: returns
{ accessToken, tokenType }. After refresh, call user/household endpoints to populate app state. Usewhoamito validate the active route’s slug when needed. - Cleanup/migration tasks:
- Remove X-Household-Slug header injection in
api/client.tsand refactor services to accepthouseholdSlugvia typed params. (Completed) - Replace temporary raw fetch calls with generated typed endpoints where available: invitations and members listing are now typed; migrate usages. Persons and parse remain raw for now.
- Integrate the new
POST /shopping/current/ingredientsendpoint into the SDK (requestIngredient(ingredientId: number)) and expose viauseShopping; refactorMyShoppingPage.vueaccordingly and remove legacy stubs. (SDK + composable done; UI refactor next) - Identity:
Userremains the primary identity.Personhas been removed; meal-related UIs useMemberRefexclusively.
- Remove X-Household-Slug header injection in
Migration Plan & Feature Flag
- Removed
VUE_APP_MULTITENANT_ENABLEDand legacy flat routes. The app now always uses slug-scoped routes.
Acceptance Criteria (Summary)
- Users can create accounts, login (email/password), logout, and refresh sessions (token-only).
- All routes operate under
/:householdSlugwith correct redirects and deep link support. - Active household is selectable and visible; API calls are correctly scoped via path params.
- Invitation token acceptance adds membership and navigates appropriately.
- Legacy persons identity is no longer used in auth; MemberRef used in meal UIs; tests updated and passing.
Google OAuth is planned next.
Open Questions / Next Steps
- Add a user profile endpoint and load it post-refresh to populate
currentUser()with real data instead of a placeholder. - Replace temporary raw fetch calls (persons, parse, members listing) with typed endpoints when available.
- Implement Google OAuth login and account creation flows.
- MyShopping ad-hoc item requests (planned):
- The v2 OpenAPI spec is being updated to include endpoints for creating, listing, updating, and deleting ad-hoc requested items on the current shopping list (independent of meals), scoped under
/api/v1/households/{householdSlug}/shopping/current/items. - Once available, add typed SDK methods:
createRequestedItem(input: { name: string; line?: string; quantity?: number; unit?: Unit })updateRequestedItem(id: number, patch: { name?: string; line?: string; quantity?: number; unit?: Unit })deleteRequestedItem(id: number)listRequestedItems()(if provided separately; otherwise rely ongetCurrentShoppingList())
- Refactor
src/components/shopping/MyShoppingPage.vueto use the above methods and remove legacy stubsgetMyShoppingList/saveMyShoppingListfrom the SDK anduseShoppingcomposable. - Update tests to drive TDD:
- MSW handlers for the new endpoints with path-scoped URLs and Authorization.
- Component tests to add, edit, and delete an ad-hoc requested item and verify it appears under outstanding items in
getCurrentShoppingList().
- Acceptance criteria:
- Users can add an item without associating it to a meal.
- Users can edit and delete such items.
- All calls use typed path-scoped endpoints; no header injection; Authorization still via provider.
- Note: No temporary measures required; proceed directly once backend ships endpoints.
- Current status: Legacy stubs removed from composable;
MyShoppingPage.vuesimplified aroundrequestIngredient; shape test added and passing. Full ad-hoc CRUD UI awaits backend endpoints.
- The v2 OpenAPI spec is being updated to include endpoints for creating, listing, updating, and deleting ad-hoc requested items on the current shopping list (independent of meals), scoped under
- Remove the legacy username login shim (
login(username: string)) and any fallback UI; standardize on email/password (and Google) only. - Rollout flag: default
VUE_APP_MULTITENANT_ENABLEDto true across environments and plan removal of legacy flat routes and related tests once stable.