--- phase: 04-shared-lists-live-sync plan: 01 type: execute wave: 1 depends_on: [] files_modified: - apps/pwa/package.json - apps/api/package.json - apps/api/src/db/schema.ts - apps/api/src/db/migrations/0002_lists_schema.sql - apps/api/test/setup.ts - apps/api/vitest.config.ts - apps/api/src/routes/lists.test.ts - apps/api/src/lib/listEmitter.test.ts - apps/pwa/src/hooks/useListSSE.test.ts - apps/pwa/src/routes/ListDetail.test.tsx - apps/pwa/src/App.tsx - apps/pwa/src/components/BottomTabBar.tsx - apps/pwa/src/routes/ListsIndex.tsx - apps/pwa/src/store/listsStore.ts autonomous: false requirements: [LIST-01, LIST-02, LIST-03, LIST-04] user_setup: [] must_haves: truths: - "User can tap a 'Lists' tab in a bottom tab bar (D-16) and land on a /lists route served by react-router (D-17)" - "The /lists route renders an empty state when no lists exist" - "The new lists/list_items/list_shares tables exist in MariaDB after migration" - "API test harness runs and the Phase 4 RED test stubs execute (failing, not erroring on import)" artifacts: - path: "apps/api/src/db/schema.ts" provides: "lists, listShares, listItems Drizzle tables" contains: "export const lists" - path: "apps/api/src/db/migrations/0002_lists_schema.sql" provides: "additive CREATE TABLE migration for the three list tables" contains: "CREATE TABLE" - path: "apps/pwa/src/components/BottomTabBar.tsx" provides: "Calendar | Lists bottom tab navigation" min_lines: 25 - path: "apps/pwa/src/routes/ListsIndex.tsx" provides: "Lists surface with empty state" min_lines: 20 - path: "apps/api/src/routes/lists.test.ts" provides: "RED test stubs for LIST-01/02/03/04 API behavior" contains: "describe" key_links: - from: "apps/pwa/src/App.tsx" to: "/lists" via: "react-router Route + BottomTabBar NavLink" pattern: "lists" - from: "apps/api/src/db/schema.ts" to: "MariaDB" via: "drizzle-kit generate + migrate" pattern: "mysqlTable\\('lists'" --- Establish the Phase 4 foundation as a thin, runnable end-to-end shell: install the four new npm dependencies, add the three list tables to the Drizzle schema and apply them via a generated migration, scaffold the API test harness with the Phase 4 Wave-0 RED test stubs, and add react-router + a bottom tab bar so the user can navigate to a (currently empty) Lists surface. This is the MVP first slice: after this plan a real user can tap "Lists" and see the Lists surface render (empty state). No list data yet — later slices fill it in. Wave 0 test stubs are created here so every downstream task has an `` target per 04-VALIDATION.md. Purpose: De-risk the transport/routing/schema/test plumbing before any list feature is built, and satisfy the [BLOCKING] generate+migrate schema constraint once for all later DB-dependent work. Output: New deps installed; three tables migrated; API vitest harness + 4 RED stub test files; router + BottomTabBar + ListsIndex empty state; listsStore (UI-only). ## Phase Goal **As a** household member, **I want to** create and manage shared named lists with real-time co-edit sync, **so that** my partner and I see each other's list edits appear within seconds without refreshing. (This plan delivers the navigable shell; later plans fill in CRUD, reorder, and live sync.) @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-shared-lists-live-sync/04-CONTEXT.md @.planning/phases/04-shared-lists-live-sync/04-RESEARCH.md @.planning/phases/04-shared-lists-live-sync/04-PATTERNS.md @.planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md @.planning/phases/04-shared-lists-live-sync/04-VALIDATION.md Task 1: Package legitimacy gate for the SUS-flagged react-router apps/pwa/package.json - .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Package Legitimacy Audit" Nothing yet — this gate precedes the install in Task 2. Per the Package Legitimacy Audit, three packages (@dnd-kit/core, @dnd-kit/sortable, fractional-indexing) are verdict OK and auto-approved. `react-router` is flagged SUS only because version 7.17.0 was published 2026-06-04 (version-recency false positive); the package is the canonical React Router (remix-run, ~12 yrs, 47.5M/wk). Surface this to the operator for a one-time confirm before installing, since legitimacy checkpoints are never auto-approvable. 1. Open https://www.npmjs.com/package/react-router and confirm publisher is `remix-run`/`react-router` org with multi-year history and ~47M weekly downloads. 2. Confirm version 7.x is the current major. - Operator types "approved" (or names a pinned version) before Task 2 runs. Type "approved" to proceed with the install, or specify an alternate version. Task 2: Install new dependencies + scaffold API test harness with Wave-0 RED stubs apps/pwa/package.json, apps/api/package.json, apps/api/test/setup.ts, apps/api/vitest.config.ts, apps/api/src/routes/lists.test.ts, apps/api/src/lib/listEmitter.test.ts, apps/pwa/src/hooks/useListSSE.test.ts, apps/pwa/src/routes/ListDetail.test.tsx - apps/api/vitest.config.ts - apps/api/src/db/client.ts - apps/pwa/src/api/client.test.ts (existing PWA test convention) - .planning/phases/04-shared-lists-live-sync/04-VALIDATION.md §"Wave 0 Requirements" and §"Per-Task Verification Map" - .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Standard Stack" → "New Dependencies" Install PWA deps: react-router@7, @dnd-kit/core, @dnd-kit/sortable, fractional-indexing via `pnpm --filter @familysync/pwa add`. Install API dep fractional-indexing via `pnpm --filter @familysync/api add` (needed server-side for rank generation). Do NOT install ioredis — fan-out is in-memory EventEmitter per RESEARCH discretion (justified in Plan 02). Scaffold the API test harness: the API currently has zero test files. Create `apps/api/test/setup.ts` and reference it from `apps/api/vitest.config.ts` (`test.setupFiles`). The setup file must establish how DB-backed route tests connect — point at the local MariaDB via the existing `apps/api/src/db/client.ts` pool (DB_HOST/DB_NAME from env), and provide a per-test cleanup (truncate lists/list_items/list_shares between tests). Pure-logic tests (listEmitter, fractional rank) do NOT need the DB. Create the four Wave-0 RED stub test files listed in 04-VALIDATION.md, each with `describe`/`it.todo` or `it(... )` blocks that compile and FAIL (red) rather than error on import — they import the not-yet-existing modules behind a guard or use `it.todo` placeholders that downstream plans convert to real assertions: - apps/api/src/routes/lists.test.ts — LIST-01/02/03/04 API behavior stubs - apps/api/src/lib/listEmitter.test.ts — scoped fan-out correctness (D-04) stubs - apps/pwa/src/hooks/useListSSE.test.ts — D-11 bounded backoff (mock EventSource) stubs - apps/pwa/src/routes/ListDetail.test.tsx — D-07 optimistic update + rollback stubs pnpm --filter @familysync/api test 2>&1 | grep -Eiq 'todo|fail|no tests|passed' && pnpm --filter @familysync/pwa exec vitest run src/hooks/useListSSE.test.ts src/routes/ListDetail.test.tsx 2>&1 | grep -Eiq 'todo|fail|passed' - `react-router`, `@dnd-kit/core`, `@dnd-kit/sortable`, `fractional-indexing` appear in apps/pwa/package.json dependencies. - `fractional-indexing` appears in apps/api/package.json dependencies. - `ioredis` is NOT added to either package.json. - `apps/api/test/setup.ts` exists and is referenced by `setupFiles` in apps/api/vitest.config.ts. - All four Wave-0 test files exist and run (todo/red), not import-error. New deps installed (no ioredis), API test harness runs, four RED/todo stub files present and executing. Task 3: Add list tables to schema and apply via generate+migrate [BLOCKING] apps/api/src/db/schema.ts, apps/api/src/db/migrations/0002_lists_schema.sql - apps/api/src/db/schema.ts (full file — table conventions) - apps/api/src/db/migrations/0001_calendars_user_url_unique.sql (prior migration shape) - apps/api/drizzle.config.ts - .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Database Schema Design" - .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"apps/api/src/db/schema.ts" - $HOME/.claude/projects/-home-luc-Projects-familysync/memory/drizzle-mariadb-push-unsafe.md Append three tables to apps/api/src/db/schema.ts following the exact conventions in 04-RESEARCH §Database Schema Design and the analog patterns in 04-PATTERNS: - `lists`: int autoincrement PK, `ownerId` int('owner_id') references users.id onDelete cascade notNull, `name` varchar(255) notNull, `isShared` boolean('is_shared') default true notNull (D-01), `createdAt` timestamp defaultNow notNull, `updatedAt` timestamp defaultNow onUpdateNow; index idx_lists_owner_id on ownerId. - `listShares` (D-02, member-count-agnostic join table): int PK, `listId` int('list_id') references lists.id onDelete cascade notNull, `userId` int('user_id') references users.id onDelete cascade notNull, `createdAt` timestamp defaultNow notNull; unique('uniq_list_share') on (listId, userId), index idx_list_shares_user_id on userId. - `listItems`: int PK, `listId` int('list_id') references lists.id onDelete cascade notNull, `text` varchar(500) notNull, `checked` boolean default false notNull, `rank` varchar(255) notNull (D-13 fractional-indexing string), `createdAt`, `updatedAt`; index idx_list_items_list_id_rank on (listId, rank), index idx_list_items_list_id_checked on (listId, checked). Then generate and apply the migration. This is [BLOCKING]: run `pnpm --filter @familysync/api db:generate` to produce `apps/api/src/db/migrations/0002_lists_schema.sql` (+ journal/snapshot). REVIEW the generated SQL — it MUST be additive (CREATE TABLE only, NO DROP/TRUNCATE of existing tables). Then run `pnpm --filter @familysync/api db:migrate` to apply. NEVER run `db:push` / `drizzle-kit push` — it emits a false destructive diff on populated MariaDB (hard project constraint). Build/type checks pass without the live migration, so this task is mandatory and must complete before any DB-dependent verification in later plans. grep -q "mysqlTable('lists'" apps/api/src/db/schema.ts && grep -q "mysqlTable('list_shares'" apps/api/src/db/schema.ts && grep -q "mysqlTable('list_items'" apps/api/src/db/schema.ts && test -f apps/api/src/db/migrations/0002_lists_schema.sql && grep -iq 'CREATE TABLE' apps/api/src/db/migrations/0002_lists_schema.sql && ! grep -iE 'DROP TABLE `?(users|calendars|calendar_events|calendar_outbox|member_credentials)' apps/api/src/db/migrations/0002_lists_schema.sql && pnpm --filter @familysync/api typecheck - Three tables present in schema.ts with the column/index/FK shapes above. - 0002_lists_schema.sql exists, contains CREATE TABLE for lists/list_items/list_shares, and contains NO DROP/TRUNCATE of any pre-existing table. - `db:migrate` applied successfully (migration recorded in drizzle journal). - `pnpm --filter @familysync/api typecheck` passes. list/list_items/list_shares tables exist in MariaDB via additive generate+migrate; typecheck green; no push used. Task 4: Add react-router + BottomTabBar + empty ListsIndex shell apps/pwa/src/App.tsx, apps/pwa/src/components/BottomTabBar.tsx, apps/pwa/src/routes/ListsIndex.tsx, apps/pwa/src/store/listsStore.ts - apps/pwa/src/App.tsx (current one-liner) - apps/pwa/src/components/CalendarShell.tsx (state-branch + data-fetch conventions) - apps/pwa/src/components/AppNav.tsx (nav/active-state + CSS token conventions) - apps/pwa/src/store/calendarStore.ts (Zustand shape convention) - apps/pwa/vite.config.ts (confirm navigateFallback already covers /lists/*) - .planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md §"Layout: App Shell Changes", §"BottomTabBar", §"ListsIndex", §"ListsEmptyState" - .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"apps/pwa/src/App.tsx", §"BottomTabBar.tsx", §"ListsIndex.tsx", §"listsStore.ts" Transform App.tsx into a BrowserRouter shell (react-router declarative mode, NO data router/loaders): routes `/` → Navigate replace to `/calendar`, `/calendar` → CalendarShell, `/lists` → ListsIndex, `/lists/:listId` → ListDetail. ListDetail does not exist yet — for this plan render a temporary placeholder route element (a stub component that says the list view is coming) so the route resolves; Plan 04 replaces it. Render BottomTabBar as a sibling of ``. Create BottomTabBar.tsx: fixed-bottom 56px + env(safe-area-inset-bottom), background var(--color-surface-dim), border-top var(--color-border), two equal NavLink tabs (CalendarDays→/calendar, List→/lists) with isActive callback applying accent var(--color-member-0) to icon+label and a 2px active indicator; inactive var(--color-text-muted); 13px label; ≥44px touch target; z-index 200. On desktop (≥768px) the existing AppNav sidebar remains; per UI-SPEC add a "Lists" NavLink there too (sidebar) — do this without breaking the existing AppNav signature. Create ListsIndex.tsx: full-height scrollable column, "Lists" heading, useQuery(['lists'], fetchLists) where fetchLists is imported from a minimal listsClient (create only the fetchLists function + List type here if listsClient does not yet exist; Plan 03 expands it). Render ListsEmptyState ("No lists yet" / "Tap + to create your first shared list…") when there are zero lists; render a placeholder card stack otherwise. Wire isLoading/isError/success branches mirroring CalendarShell. Include the "+ New List" FAB affordance (non-functional placeholder is acceptable here; Plan 03 wires CreateListSheet). Create listsStore.ts (Zustand, UI-only): activeTab and createListSheetOpen state with setters, following calendarStore conventions (no persist, no immer). Confirm vite.config.ts navigateFallback ('/index.html') + denylist already cover SPA deep-links to /lists/* (it does per Phase 3 config) — if a denylist entry would block /lists, fix it; otherwise leave unchanged and note in SUMMARY. pnpm --filter @familysync/pwa exec vitest run src/components/CalendarShell.test.tsx 2>&1 | grep -Eiq 'passed' && pnpm --filter @familysync/pwa exec tsc --noEmit && grep -q "BrowserRouter" apps/pwa/src/App.tsx && grep -q "to=\"/lists\"" apps/pwa/src/components/BottomTabBar.tsx - App.tsx wraps the app in BrowserRouter with /calendar, /lists, /lists/:listId routes; existing CalendarShell still mounts at /calendar. - BottomTabBar renders Calendar and Lists NavLinks with active-state accent and ≥44px targets. - ListsIndex renders the empty state copy from UI-SPEC when no lists exist. - listsStore exports activeTab/createListSheetOpen with setters (no server data). - PWA typecheck passes; existing CalendarShell test still green. - Browser check (project convention): `playwright-cli` navigates to /lists and observes the "No lists yet" empty state and the bottom tab bar with an active "Lists" tab. Record the observation in SUMMARY. User can tap the Lists tab and land on the empty Lists surface; calendar still works; router + tab bar in place. ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser → /api/* | All list/SSE requests cross here; untrusted client input | | API → MariaDB | Drizzle parameterized queries only | | drizzle-kit → MariaDB (migration) | DDL applied to a populated production-shaped DB | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-04-01 | Tampering | drizzle-kit push truncating populated tables | mitigate | generate+migrate ONLY; verify 0002 SQL has no DROP/TRUNCATE of existing tables before applying (Task 3 gate) | | T-04-02 | Information Disclosure | scoped-fan-out leak (D-04) — foundational | mitigate | Schema models access via list_shares join table + owner_id (this plan); enforcement lands in Plans 02/03/06; negative test seeded in lists.test.ts here | | T-04-SC | Tampering | npm installs (react-router SUS, dnd-kit, fractional-indexing) | mitigate | Legitimacy audit in RESEARCH; blocking human checkpoint (Task 1) for the SUS react-router before install | - `pnpm --filter @familysync/api typecheck` and `pnpm --filter @familysync/pwa exec tsc --noEmit` both pass. - `pnpm --filter @familysync/api test` runs (Wave-0 stubs red/todo, not erroring). - 0002_lists_schema.sql is additive; migration applied; three tables queryable. - `playwright-cli` confirms /lists renders the empty state with the bottom tab bar. - New deps installed (no ioredis); API test harness operational. - Three list tables migrated additively (no push). - Router + BottomTabBar live; Lists tab navigates to an empty Lists surface. - Four Wave-0 RED stub test files exist and execute. **Symbols/files this plan creates (exclude from drift verification — they are new):** - Tables: `lists`, `list_shares`, `list_items` (apps/api/src/db/schema.ts) - Migration: `apps/api/src/db/migrations/0002_lists_schema.sql` (+ journal/snapshot) - API test harness: `apps/api/test/setup.ts`; setupFiles wiring in `apps/api/vitest.config.ts` - RED stub tests: `apps/api/src/routes/lists.test.ts`, `apps/api/src/lib/listEmitter.test.ts`, `apps/pwa/src/hooks/useListSSE.test.ts`, `apps/pwa/src/routes/ListDetail.test.tsx` - Components: `BottomTabBar` (apps/pwa/src/components/BottomTabBar.tsx), `ListsIndex` (apps/pwa/src/routes/ListsIndex.tsx), temporary ListDetail placeholder route element - Store: `useListsStore` (apps/pwa/src/store/listsStore.ts) with activeTab/createListSheetOpen - App.tsx now exports a BrowserRouter-wrapped App + AppShell - (Possibly) initial `apps/pwa/src/api/listsClient.ts` with `fetchLists` + `List` type Create `.planning/phases/04-shared-lists-live-sync/04-01-SUMMARY.md` when done.