14 KiB
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
-
[~] 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.currentUser()remains a bridge to cookie-based refresh; adapts legacyPersontoUsertemporarily.
- 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
-
[~] Update Router for Multi-Tenancy:
- Modify
src/router/index.ts:- Added new public routes:
/create-account,/welcome, and/invitations/accept. - Feature flag
VUE_APP_MULTITENANT_ENABLEDcontrols nesting:- When enabled: feature routes are nested under
/:householdSlug/.... - When disabled: legacy flat routes remain for backward compatibility.
- When enabled: feature routes are nested under
- Refactor the
beforeEachguard:- Allows public routes.
- When multitenant flag is on, 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.
- Nest existing routes: Implemented behind feature flag.
- Added new public routes:
- Modify
-
[~] 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 is implemented with email/displayName/password form callinguseAuth.createAccount; Invitation Accept remains TODO.
-
[~] 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: Implemented header-injection in
src/api/client.tsviaX-Household-SlugandAuthorizationusing configurable providers; no path changes.- Verified by
tests/household.header.test.tsand auth tests.
- Verified by
- Create
-
[ ] Implement Invitation UI:
- Build the
HouseholdSettings.vueview for inviting members and listing current members.
- Build the
-
[ ] 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 2025) and Gap Analysis
Current implementation is single-tenant with username-based login and no household context.
What exists today
- Auth
src/api/auth.ts: username login (login(username: string)),currentUser()via cookieuser_id.src/composables/useAuth.ts: storesPerson | null, exposeslogin(username)andloadUser().src/components/LoginPage.vue: lists persons and logs in by selected person name.
- Routing
src/router/index.ts: flat routes (/recipes,/shopping,/mealplan); no/:householdSlugnesting or redirects.- Guard checks
requiresAuthonly; no household awareness.
- SDK/API
src/api/sdk.ts: calls/api/v1/...without household context.- Generated OpenAPI types do not include household slug in paths; shapes are single-tenant.
- Domain & UI
- Identity revolves around
Person; no household model, switcher, or settings views.
- Identity revolves around
Gaps vs requirements
- Authentication & onboarding
- Implemented: JWT
loginWithPassword,logout, account creation viacreateAccountwith token provider wiring. - UI:
LoginPage.vueshows email/password form under feature flag;CreateAccount.vueview implemented;Welcome.vuecreates household; Invitation Accept pending; Google OAuth pending.
- Implemented: JWT
- Routing & URL-based tenancy
- Partial: Feature-flagged nesting implemented; still need guard logic for fetching households and redirects.
useHousehold.tsandHouseholdSwitcher.vueadded; further wiring to fetch households pending.
- API boundary
- No household scoping passed to backend. Need a typed strategy (prefer header parameter) without breaking OpenAPI typing.
- Cleanup
personsused across login and tests; must be deprecated in favor of authenticateduserand their households.
Assumptions and constraints
- Preserve strict TS (no
any/unknownin app code) and keep OpenAPI as source of truth. - Do not rewrite typed API paths in code; prefer header or OpenAPI param for household scoping.
Amendment: API Services and Typing Strategy
To align with OpenAPI typing and README axioms, do not rewrite request paths to include the household slug.
- Preferred: Backend exposes
householdSlugas a path or header parameter in OpenAPI. Regenerate types and thread viaparamsfor each call. - Interim: Agree a header (e.g.,
X-Household-Slug) and inject it insrc/api/client.tsfor all requests based on the current route’s slug. Keep existing typed paths untouched.
Action
- Implement header injection in
api/client.tswith a pluggable getter for the active slug (decoupled from Vue imports). Update this spec once backend finalizes the parameter shape.- DONE: Implemented with
setHouseholdSlugProvider. Will align to OpenAPI when backend finalizes.
- DONE: Implemented with
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 feature-flagged nested routes and new public routes. Placeholders for onboarding/invitations added.
- Implemented
useHousehold.ts, header + auth providers in API client, minimalHouseholdSwitcher.vue, and mounted it. Added a header injection test. - Implemented JWT login/register in
auth.tsand wired token to client provider.useAuthupdated with households fetching. - Router guard updated to handle public/multitenant routing and redirects.
- Added
useAuth.createAccountwith state update and tests for it; implementedCreateAccount.vuewith form and navigation. - Next: Implement Invitation Accept flow and Household Settings (invite members form), then remove legacy Person UI.
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:- Add header injection (
X-Household-Slug) from a configurable getter to scope requests. - Keep
pathstyping intact; no path string mutations.
- Add header injection (
SDK
src/api/sdk.ts:- No path changes; ensure all calls work with new auth and household header.
UI
- Login Page: Refactored to show email/password form when multitenant flag is enabled; legacy person list retained otherwise. 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.
Tests
- Update MSW handlers/tests to assume JWT auth and household header.
- Add tests for router guards, invitation acceptance, and household switching.
OpenAPI & Typing Considerations
- Avoid
any/unknownin app code; keep all API calls typed viaopenapi-fetch. - If backend adds header parameter to OpenAPI, regenerate and remove any client-specific header wiring.
Migration Plan & Feature Flag
- Optional
MULTITENANT_ENABLEDflag for staged rollout of routes and UI. - Keep legacy login until backend endpoints are ready; hide persons UI once households exist for a user.
Acceptance Criteria (Summary)
- Users can create accounts, login (email/password, Google), logout, and refresh sessions.
- All routes operate under
/:householdSlugwith correct redirects and deep link support. - Active household is selectable and visible; API calls are correctly scoped.
- Invitation token acceptance adds membership and navigates to the household dashboard.
- Legacy persons login removed from UI; tests updated and passing.
Open Questions
- Backend: path vs header vs cookie for
householdSlug? Confirm to finalize client strategy. - Exact OpenAPI shapes for
User,Household,Invitationendpoints. - Google OAuth flow pattern (token exchange vs redirect).
- JWT storage medium per security guidance (cookie vs localStorage).