SP‑A Multi-Household Foundation — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Let one auth account belong to multiple households, with an explicit server-persisted active household and a minimal switcher (a tappable household name atop the More page), so the account can create household A, create household B, and switch between them.
Architecture: The data model + RLS are already multi-household-capable (household_members is the join; member_household_ids() is set-returning). SP‑A adds one small account_active_household table, makes getHousehold() active-household-aware (reads that table, falls back to the newest-joined heuristic), adds setActiveHousehold/listMyHouseholds across the SDK's 5 layers, narrows the createHousehold idempotency guard so a 2nd household is allowed (auto-set active), and wires a More-tab switcher that re-bootstraps the app on switch (reusing the existing sign-in bootstrapSession → getHouseholdById re-hydrate seam) and resets the member "viewing-as" lens.
Tech Stack: Flutter 3.44 / Dart 3.9 (FVM), the client_sdk 5-layer stack (Client facade → ClientImpl → HouseholdService → StoragePort → {cloud supabase_households, in-memory, cached} adapters), flutter_bloc, auto_route, client_sdk_testing doubles, the flow-test harness, Supabase (prod bgedvvmihygwxhjxlvfu).
Global Constraints
- FVM only:
fvm flutter .../fvm dart ...— never bare. App test cmds run fromapp/; SDK test cmds frompackages/client_sdk/. - One data path: Bloc/Repository → Client facade → Service → StoragePort → adapter. Presentation never imports
drift/supabase. - Enforce in service AND schema: active-household membership is validated in
HouseholdService(a caller may only set/resolve a household they're a member of); RLS self-scopes the new table. - Per-household member rows; shared cross-household identity is OUT of scope (deferred post-MVP).
- Migration is file-only. Write the SQL under
infra/supabase/migrations/; applying it to prod is deploy-gated — do NOT apply via the Supabase MCP without explicit owner authorization. SDK/app tests use the in-memory adapter (no SQL), so the suite is green without the migration applied. - Error handling: typed
on <SpecificException>clauses only — never barecatch (e), never catchError. - Codegen: if a state/model gains
@JsonSerializablefields, regen the specific.g.dartwith a scoped--build-filter, thengit status+ restore any collateral generated file. (This plan avoids new serializable models — see Task 6.) - Baselines must not drop. Record the current SDK + app suite counts at Task 1 start and keep them at/above baseline. Run the FULL relevant suite at the end of any task that changes a shared interface (
StoragePort,Client,HouseholdService) — a signature change there touches all adapters + call sites. - Brand-neutral names; all copy via
Strings; no!bang on nullable.
Resolved decisions (controller — do not re-litigate)
listMyHouseholds(authUserId)→Future<List<Household>>(full model; active flag derived by the caller comparing to the resolved active household — no newHouseholdSummary).- Switch re-scope = write the active row, then re-bootstrap via
bootstrapSession(authUserId)(itsgetHouseholdByIdre-hydrates the cached adapter). Norehydratemethod onStoragePort. createHouseholdauto-sets the new household active (callsupsertActiveHouseholdafter insert).getHousehold()becomes active-aware (readsgetActiveHouseholdId);AuthenticatedGuardis unchanged.SetupPagegainsisCreatingAdditional: bool = false(skips the invite-code CTA; completion relies on auto-active create).
File Structure
Create:
infra/supabase/migrations/20260723000000_account_active_household.sql— the table + RLS.app/lib/inside/routes/authenticated/more/widgets/household_switcher_sheet.dart— the picker sheet.- Tests:
packages/client_sdk/test/...(adapter + service),app/test/flows/household_switch_test.dart.
Modify (exact files, from the grounding audit):
packages/client_sdk/lib/src/adapters/adapter.dart—StoragePort: +getActiveHouseholdId,upsertActiveHousehold,listHouseholdsForAuthUser.packages/client_sdk/lib/src/adapters/memory/in_memory_storage_adapter.dart—_activeHouseholdsmap + 3 impls + active-awaregetHousehold.packages/client_sdk/lib/src/adapters/cloud/supabase_households.dart— 3 impls + active-awaregetHousehold.packages/client_sdk/lib/src/adapters/cached/cached_storage_adapter.dart— 3 impls (delegate;upsertActiveHouseholdre-hydrate not required — see Task 3).packages/client_sdk/lib/src/services/household_service.dart—setActiveHousehold,listMyHouseholds, active-awaregetHousehold,createHouseholdauto-active.packages/client_sdk/lib/src/client/client.dart+client_impl.dart— 2 new facade verbs + delegation.app/lib/outside/repositories/household/household_repository.dart— passthroughs.app/lib/inside/blocs/more/bloc.dart(+ state) — loadlistMyHouseholds; switch event.app/lib/inside/routes/authenticated/more/widgets/household_header_block.dart—onTap+ chevron.app/lib/inside/routes/authenticated/setup/page.dart—isCreatingAdditional.app/lib/inside/i18n/strings.dart— switcher copy.
Task 1: account_active_household migration (file-only)
Files: Create infra/supabase/migrations/20260723000000_account_active_household.sql
Interfaces: Produces the table public.account_active_household(auth_user_id uuid pk → auth.users, active_household_id uuid not null → households, updated_at) with self-scoped RLS. Consumed by Task 3's cloud adapter (deploy-gated); SDK/app tests use the in-memory adapter and do NOT need this applied.
- Step 1: Write the migration (verbatim from the approved spec):
create table public.account_active_household (
auth_user_id uuid primary key references auth.users (id) on delete cascade,
active_household_id uuid not null references public.households (id) on delete cascade,
updated_at timestamptz not null default now()
);
alter table public.account_active_household enable row level security;
create policy account_active_household_select on public.account_active_household
for select to authenticated using (auth_user_id = auth.uid());
create policy account_active_household_upsert on public.account_active_household
for insert to authenticated with check (auth_user_id = auth.uid());
create policy account_active_household_update on public.account_active_household
for update to authenticated using (auth_user_id = auth.uid())
with check (auth_user_id = auth.uid());
-
Step 2: Lint the SQL (no apply). Confirm it parses locally if a linter exists (e.g.
sqlfluffif configured), else visual review against the sibling migration20260612000001_households.sqlfor style parity (lowercase,public.schema, RLS enabled + policies). Do NOT apply to prod. -
Step 3: Commit
git add infra/supabase/migrations/20260723000000_account_active_household.sql
git commit -m "feat(sp-a): account_active_household table + self-scoped RLS (file-only, deploy-gated)"
The migration apply to prod
bgedvvmihygwxhjxlvfuis a SEPARATE, owner-authorized deploy step (record it in the ledger as pending). The rest of SP‑A is testable without it via the in-memory adapter.
Task 2: StoragePort contract + in-memory adapter
Files:
- Modify:
packages/client_sdk/lib/src/adapters/adapter.dart(StoragePort) - Modify:
packages/client_sdk/lib/src/adapters/memory/in_memory_storage_adapter.dart - Test:
packages/client_sdk/test/adapters/in_memory_active_household_test.dart(or extend the existing in-memory adapter test)
Interfaces:
-
Produces on
StoragePort:Future<String?> getActiveHouseholdId(String authUserId);Future<void> upsertActiveHousehold({required String authUserId, required String householdId});Future<List<Household>> listHouseholdsForAuthUser(String authUserId). -
Consumed by Task 3 (other adapters implement the same contract), Task 4 (
getHouseholdactive-aware), Task 5 (service). -
Step 1: Write the failing test — in-memory adapter round-trips the active household + lists memberships:
// match the existing in-memory adapter test harness (imports, seed factories)
test('upsert + getActiveHouseholdId round-trips per auth user', () async {
final a = InMemoryStorageAdapter();
// seed: authUser 'u1' member of households 'h1' and 'h2' (use the adapter's seed path)
await a.upsertActiveHousehold(authUserId: 'u1', householdId: 'h2');
expect(await a.getActiveHouseholdId('u1'), 'h2');
expect(await a.getActiveHouseholdId('u2'), isNull);
});
test('listHouseholdsForAuthUser returns all memberships', () async {
final a = InMemoryStorageAdapter();
// seed u1 in h1 + h2
final list = await a.listHouseholdsForAuthUser('u1');
expect(list.map((h) => h.id), containsAll(<String>['h1', 'h2']));
});
Read
in_memory_storage_adapter.dartfirst to match its real seed mechanism (how_members/_householdsget populated in tests) and theHouseholdconstructor.
-
Step 2: Run → FAIL (
cd packages/client_sdk && fvm dart test test/adapters/in_memory_active_household_test.dart). -
Step 3: Add the 3 abstract methods to
StoragePort(adapter.dart), with doc comments mirroring the neighbours (e.g. neargetHouseholdById/memberByAuthUserId):
/// The account's explicitly-selected active household id, or null if unset
/// (callers fall back to the newest-joined heuristic).
Future<String?> getActiveHouseholdId(String authUserId);
/// Persist [householdId] as [authUserId]'s active household (upsert).
Future<void> upsertActiveHousehold({
required String authUserId,
required String householdId,
});
/// Every household [authUserId] is a member of.
Future<List<Household>> listHouseholdsForAuthUser(String authUserId);
-
Step 4: Implement in the in-memory adapter: add
final Map<String, String> _activeHouseholds = {};; implement the three methods (upsertsets the map;getreads it;listfilters_membersbyauthUserId→ collect their_households). Match the file's existing async style. -
Step 5: Run the test → PASS, then the full SDK suite (
cd packages/client_sdk && fvm dart test) — adding abstract methods toStoragePortmeans EVERY adapter must implement them or the package won't compile; Tasks 3 implements the others, so at THIS task the cloud/cached adapters will not yet compile. To keep the package compiling within Task 2, add temporaryUnimplementedErrorstubs for the 3 methods on the cloud + cached adapters (they get real impls in Task 3) — OR sequence Task 3 immediately and treat 2+3 as one compile unit. Chosen: addthrow UnimplementedError()stubs in cloud + cached here (clearly commented// SP-A Task 3), so the in-memory tests pass and the package compiles; Task 3 replaces them. -
Step 6: Commit
git add packages/client_sdk/lib/src/adapters/ packages/client_sdk/test/adapters/in_memory_active_household_test.dart
git commit -m "feat(sp-a): StoragePort active-household verbs + in-memory impl (cloud/cached stubbed)"
Task 3: Cloud + cached adapter impls
Files:
- Modify:
packages/client_sdk/lib/src/adapters/cloud/supabase_households.dart - Modify:
packages/client_sdk/lib/src/adapters/cached/cached_storage_adapter.dart - Test: the fake-PostgREST cloud-adapter test path if present (
graphify query "fake postgrest cloud adapter test"); else guarded/documented.
Interfaces: Consumes the StoragePort contract (Task 2). Produces real cloud + cached impls of the 3 verbs (replacing the Task 2 stubs).
-
Step 1: Cloud adapter — replace the stubs:
getActiveHouseholdId:select active_household_id from account_active_household where auth_user_id = :uid(maybe-single) → the id or null.upsertActiveHousehold: upsertaccount_active_household(on_conflict auth_user_id), setupdated_at = now().listHouseholdsForAuthUser:householdsjoined tohousehold_memberswhereauth_user_id = :uid→List<Household>. Use the adapter's existingdb.selectEq/upsert helpers (mirror howmemberByAuthUserId/getHouseholdByIdcall them). RLS enforces self-scope server-side.
-
Step 2: Cached adapter — the cached adapter wraps another
StoragePort. Implement the 3 verbs as pass-through delegation to the wrapped adapter (_inner.getActiveHouseholdId(...), etc.). Re-hydration on switch is handled at the app layer viabootstrapSession(Task 6), NOT insideupsertActiveHousehold— keep the port method a pure write. (Confirm the cached adapter's delegation pattern by reading how it forwards an existing method likegetHouseholdById.) -
Step 3: Tests — if a fake-PostgREST harness exists for the cloud adapter, add a round-trip test there. If not, add a focused test on the cached adapter's delegation (using a fake inner
StoragePort) asserting the 3 verbs forward correctly. Run the SDK suite (cd packages/client_sdk && fvm dart test) — now green with no stubs. -
Step 4: Commit
git add packages/client_sdk/lib/src/adapters/
git commit -m "feat(sp-a): cloud + cached adapter active-household impls"
Task 4: active-aware getHousehold
Files:
- Modify:
packages/client_sdk/lib/src/services/household_service.dart(getHousehold) AND/OR the adapters'getHousehold(see below) - Test:
packages/client_sdk/test/services/get_household_active_test.dart
Interfaces: Consumes getActiveHouseholdId + getHouseholdById + membership. Produces: getHousehold() returns the active household when set and the account is a member; else the existing newest-joined fallback.
Decision (where the active-resolution lives): put it in the adapters' getHousehold (both cloud + in-memory already implement getHousehold with the newest-joined walk — that is the right seam, and the cached adapter delegates). Rationale: the service's getHousehold is a thin _storage.getHousehold() passthrough; keeping resolution in the adapter keeps one code path and lets the cached adapter's hydration continue to work. Each adapter's getHousehold: read getActiveHouseholdId(authUserId); if non-null AND the account has a member row in it → getHouseholdById(activeId); else the current newest-joined logic.
-
Step 1: Failing test (in-memory): seed u1 in h1 + h2; with no active set →
getHousehold()returns the newest-joined (match current heuristic); afterupsertActiveHousehold(u1, h1)→ returns h1; set active to a household u1 is NOT in (h3) → falls back to newest-joined (never returns h3). Note:getHousehold()derives the auth user from the adapter's auth context — match how the in-memory adapter knows "the current auth user" today (read itsgetHouseholdimpl; if it takes no arg, thread the active lookup via the same auth id it already uses). -
Step 2: Run → FAIL.
-
Step 3: Implement the active-aware branch in the in-memory + cloud
getHousehold(cached delegates). Preserve the exact existing fallback (the "Bugfix D' newest-first" behaviour) for the unset/non-member cases. -
Step 4: Run the test + full SDK suite → green. Existing
getHouseholdcallers are unchanged (same signature; behaviour only differs when an active row exists). -
Step 5: Commit
git add packages/client_sdk/lib/src/adapters/ packages/client_sdk/test/services/get_household_active_test.dart
git commit -m "feat(sp-a): getHousehold resolves the active household (fallback preserved)"
Task 5: HouseholdService verbs + facade + createHousehold auto-active
Files:
- Modify:
packages/client_sdk/lib/src/services/household_service.dart - Modify:
packages/client_sdk/lib/src/client/client.dart+client_impl.dart - Test:
packages/client_sdk/test/services/active_household_service_test.dart
Interfaces:
-
Produces on the Client facade:
Future<List<Household>> listMyHouseholds(String authUserId);Future<void> setActiveHousehold({required String authUserId, required String householdId}). -
createHousehold(...)now callsupsertActiveHouseholdfor the creator after insert (auto-active). -
Step 1: Failing tests:
setActiveHousehold: member → calls_storage.upsertActiveHousehold; non-member → throwsDomainRuleException(validate vialistHouseholdsForAuthUser/membership before upserting).listMyHouseholds: returns the account's households.createHousehold: after creating, the creator's active household is the new one (assertgetActiveHouseholdId(creator) == newHouseholdId).
-
Step 2: Run → FAIL.
-
Step 3: Implement in
HouseholdService:
Future<List<Household>> listMyHouseholds(String authUserId) =>
_storage.listHouseholdsForAuthUser(authUserId);
Future<void> setActiveHousehold({
required String authUserId, required String householdId,
}) async {
final mine = await _storage.listHouseholdsForAuthUser(authUserId);
if (!mine.any((h) => h.id == householdId)) {
throw const DomainRuleException('Not a member of that household.');
}
await _storage.upsertActiveHousehold(authUserId: authUserId, householdId: householdId);
}
In createHousehold, after the member/household are created and creatorAuthUserId != null, call upsertActiveHousehold(authUserId: creatorAuthUserId, householdId: created.id). Narrow the existing idempotency guard: today it returns the existing household if the account has ANY member — change it so it only short-circuits a genuine in-flight duplicate (or drop the "reuse any" behaviour), allowing a 2nd household. (Read the current guard at household_service.dart and replace the "reuse any existing membership" branch; keep creation idempotent only against an exact double-submit if such a key exists, else remove the guard.)
-
Step 4: Facade + ClientImpl — add the 2 abstract declarations to
client.dart(mirrorgetHousehold's doc style) and delegate inclient_impl.dartto_householdService. -
Step 5: Run tests + FULL SDK suite → green (facade signature additions compile across
ClientImpl+MockClientinclient_sdk_testing— ifMockClientis hand-written, add the 2 methods there; if it's a mocktail mock, no change). -
Step 6: Commit
git add packages/client_sdk/lib/src/ packages/client_sdk/test/services/active_household_service_test.dart
git commit -m "feat(sp-a): setActiveHousehold + listMyHouseholds + createHousehold auto-active"
Task 6: App HouseholdRepository passthroughs + member-lens reset seam
Files:
- Modify:
app/lib/outside/repositories/household/household_repository.dart - Test:
app/test/unit/repositories/household_repository_active_test.dart
Interfaces:
-
Produces:
HouseholdRepository.listMyHouseholds(String authUserId),.setActiveHousehold({authUserId, householdId}), and.switchActiveHousehold({authUserId, householdId})— the last one does the full switch:setActiveHousehold→bootstrapSession(authUserId)(re-hydrate) → returns the newHousehold. The member-lens reset (SelectedMemberRepository.clearToSelf()) is invoked by the MoreBloc (Task 7), not here, so the repository stays a thin data delegate. -
Step 1: Failing test —
listMyHouseholds/setActiveHouseholdforward to the client;switchActiveHouseholdcallssetActiveHouseholdthenbootstrapSessionand returns the resolved household. Use aMockClient. -
Step 2: Run → FAIL.
-
Step 3: Implement the thin passthroughs (mirror the existing
HouseholdRepositorydelegate style —Future<X> foo(...) => _clientProvider.client.foo(...)):
Future<List<Household>> listMyHouseholds(String authUserId) =>
_clientProvider.client.listMyHouseholds(authUserId);
Future<void> setActiveHousehold({required String authUserId, required String householdId}) =>
_clientProvider.client.setActiveHousehold(authUserId: authUserId, householdId: householdId);
/// Full switch: persist active, then re-bootstrap (re-hydrates the cache to the
/// new household). Returns the newly-active household.
Future<Household?> switchActiveHousehold({
required String authUserId, required String householdId,
}) async {
await _clientProvider.client.setActiveHousehold(authUserId: authUserId, householdId: householdId);
return _clientProvider.client.bootstrapSession(authUserId);
}
-
Step 4: Run test + full app suite → green.
-
Step 5: Commit
git add app/lib/outside/repositories/household/household_repository.dart app/test/unit/repositories/household_repository_active_test.dart
git commit -m "feat(sp-a): HouseholdRepository active-household passthroughs + switch"
Task 7: More-tab switcher (tappable name + picker)
Files:
- Modify:
app/lib/inside/routes/authenticated/more/widgets/household_header_block.dart(tappable) - Create:
app/lib/inside/routes/authenticated/more/widgets/household_switcher_sheet.dart - Modify:
app/lib/inside/blocs/more/bloc.dart(+ state),app/lib/inside/i18n/strings.dart - Test:
app/test/flows/household_switch_test.dart
Interfaces: Consumes HouseholdRepository.listMyHouseholds/switchActiveHousehold, SelectedMemberRepository.clearToSelf, CurrentMemberRepository. Produces the switcher UI + MoreBloc switch handling.
-
Step 1: Add strings (
householdSwitcherTitle,householdSwitcherCreateAnother, etc.). -
Step 2: Make
HouseholdHeaderBlocktappable — addfinal VoidCallback? onTap;; wrap theDsCardcontent with the tap (the block's doc comment already calls it "the future HOUSEHOLD SELECTOR seat") + a trailingIcon(Icons.expand_more). Preserve its current layout/keys. -
Step 3:
HouseholdSwitcherSheet— a modal listinglistMyHouseholds(active one marked with a check, tap → switch), plus a "+ Create another household" row. Tapping a household dispatches aMoreHouseholdSwitchRequested(householdId)event; "create another" pushesSetupRoute(isCreatingAdditional: true)(Task 8). -
Step 4:
MoreBloc— loadlistMyHouseholdsonMoreStarted(store in state for the sheet); handleMoreHouseholdSwitchRequested: resolve the authedauthUserId(fromCurrentMemberRepository/auth), callHouseholdRepository.switchActiveHousehold, thenSelectedMemberRepository.clearToSelf(), then navigate to the shell root (router.replaceAll([MainShellRoute()])) so the whole app re-scopes. Guard errors withon <SpecificException>→ a themed failure (never bare catch). -
Step 5: Flow test — this pairs with Task 8's create-second-household; for Task 7 assert: with the account in two seeded households, tapping the header opens the sheet listing both (active marked), tapping the other switches (verify
switchActiveHouseholdcalled + the shell re-scopes to the other household's data). Use the flow-test harness +MockClient/in-memory stubs. -
Step 6: Run flow test + full app suite → green. Commit.
git add app/lib/inside/routes/authenticated/more/ app/lib/inside/blocs/more/ app/lib/inside/i18n/strings.dart app/test/flows/household_switch_test.dart
git commit -m "feat(sp-a): More-tab household switcher (tappable name + picker)"
Task 8: Setup re-entry for "+ Create another household"
Files:
- Modify:
app/lib/inside/routes/authenticated/setup/page.dart(+ its bloc if the CTA logic lives there) - Modify:
app/lib/inside/routes/router.dart+router.gr.dart(SetupRoute gains the param — scoped regen) - Test: extend
app/test/flows/household_switch_test.dart
Interfaces: Consumes createHousehold (now auto-active, Task 5). Produces SetupPage({isCreatingAdditional = false}) that, when true, hides the invite-code CTA and on completion navigates to the shell (the created household is already active).
-
Step 1: Failing flow test — create household A (first-run Setup) → from the More switcher tap "+ Create another" → Setup opens in
isCreatingAdditionalmode (no invite CTA) → name household B → land on B's shell → open switcher → both A and B listed → switch back to A re-scopes. (One end-to-end flow proving the SP‑A goal.) -
Step 2: Run → FAIL.
-
Step 3: Add
isCreatingAdditionaltoSetupPage(@RoutePageparam) — when true, hide the invite-code secondary CTA; the completion path already lands on the shell, and becausecreateHouseholdauto-sets the new household active (Task 5), the guard resolves B. Scoped router regen:cd app && fvm dart run build_runner build --build-filter "lib/inside/routes/router.gr.dart"→git statusrestore collateral. -
Step 4: Run the flow test + FULL app suite → green.
-
Step 5: Update graphify + commit
git add app/lib/inside/routes/ app/test/flows/household_switch_test.dart
git commit -m "feat(sp-a): Setup re-entry for create-another-household + end-to-end switch flow"
cd .. && graphify update . && git add graphify-out/ && git commit -m "chore: graphify update after SP-A" || true
Self-Review
1. Spec coverage (against docs/superpowers/specs/2026-07-22-multi-household-foundation-sp-a-design.md):
account_active_householdtable + RLS → Task 1. ✅- active-aware
getHousehold→ Task 4. ✅ setActiveHousehold+listMyHouseholds(5 layers) → Tasks 2/3/5. ✅createHouseholdguard narrowed + auto-active → Task 5. ✅ActiveHouseholdRepository/switch + re-scope → Task 6 (folded intoHouseholdRepository.switchActiveHousehold— the spec's "ActiveHouseholdRepository" is realized as methods on the existing thinHouseholdRepositoryrather than a new class, per YAGNI; note this deviation). ✅- tappable-name switcher + create-another (reuse Setup) → Tasks 7/8. ✅
- member-lens reset on switch → Task 7 (
clearToSelf). ✅
2. Placeholder scan: each code step shows the code or an exact modify-target; commands are FVM + scoped. The > Read X first notes are verify-against-codebase instructions (the two explorers gave file:line but the implementer confirms exact current signatures), not placeholders. ✅
3. Type consistency: getActiveHouseholdId/upsertActiveHousehold/listHouseholdsForAuthUser (port) → listMyHouseholds/setActiveHousehold (service+facade) → switchActiveHousehold (app repo) used consistently. authUserId param name uniform. List<Household> (no HouseholdSummary) throughout. ✅
Cross-cutting notes for the executor
- Tasks 2+3 are a compile unit: Task 2 adds abstract
StoragePortmethods (breaks cloud/cached compile) and stubs them; Task 3 replaces the stubs. Do not leaveUnimplementedErrorpast Task 3. Run the SDK suite at the end of 3 (not just 2). - Signature-change discipline: Tasks 2 (
StoragePort) and 5 (Client/HouseholdService) change shared interfaces → every adapter +MockClientmust compile; run the full SDK suite at each. Task 8 changes a route → full app suite + scoped router regen. - Migration apply is deploy-gated — SP‑A ships + tests green WITHOUT applying it (in-memory adapter). Record "apply
20260723000000_account_active_household.sqlto prod" as a pending owner-authorized step. - Deferred (not SP‑A): SP‑B join flow, SP‑C merge, SP‑D polished admin, shared cross-household identity.