Dynamic — key flows
How a request actually moves through the layers. These are the flows worth knowing cold.
1. Chore completion → approval → ledger
A completion always writes a ChoreSubmission; whether an approver is in the
loop depends on the chore's ApprovalPolicy. The manual approver is a
parent OR a Helper — both hold the approveCompletion capability, stamped
parentManual or helperManual respectively (spend-request and goal approvals stay
parental/admin-only). Both branches converge on the same single token-moving path
(ApprovalService → LedgerService).
2. Spend request → redemption
3. Cache-first read (write-through)
The offline-first read path: serve from cache, fall through to the store on a miss.
4. Auth / onboarding → setup (tri-state guard + idempotent bootstrap)
AuthenticatedGuard must separate three states, not two, because a cold/fresh
session's identity read returns a completed-null that is indistinguishable from a
genuinely-new account: (a) loading / not-yet-resolved, (b) resolved + has member,
(c) resolved + genuinely no member. Treating (a) as (c) previously bounced an
onboarded account into the Setup wizard and stranded it there; walking that wizard —
against a non-idempotent bootstrap — minted duplicate households (three for one live
account before the fix).
The guard now routes tri-state, and a router-scoped SetupBounceLatch records whether the
guard itself drove the Setup landing. When the member later resolves
(household != null) on a guard-driven bounce, the account is rescued to the shell
instead of stranded; the latch is reset on sign-out so it never leaks across sessions.
An intentional push to Setup (a household-owning user) never sets the latch, so that flow
is untouched.
Idempotent bootstrap. HouseholdService.createHousehold checks
memberByAuthUserId(creatorAuthUserId) before inserting; if the account already has a
member it reuses that household rather than minting a second. A transport error
propagates (unknown ≠ no-member), so a blip never creates a duplicate. Multi-household
membership via acceptInvite is preserved — only the bootstrap auto-create is guarded.
Deterministic getHousehold(). Household resolution for writes now matches
memberForAccount: the newest account-linked member's household (newest-first by
created_at), consistent across all three StoragePorts — so a created activity/chore
lands in the same household the UI renders, closing the "activity disappears" symptom the
duplicate-household incident produced. (Dashed / deferred: context-free getHousehold()
cannot know the current auth uid, and the guard cannot yet distinguish (a) from (c) at
first read without an SDK-level loading/loaded/empty tri-state on the identity read — the
correct long-term hardening.)
5. Daily tempBonus expiry sweep
The app observes its own foreground/background transitions to expire
tempBonusUntil entries once per calendar day, without a background job or
server push.
Why: temp bonuses (tempBonusTokens / tempBonusUntil) give a parent a
time-limited incentive window. Expiry is idempotent and cheap — a single
where predicate over the chore table — so it runs eagerly on resume rather
than requiring a background isolate or cloud function.
Why / changed from the POC: the completion funnel is instrumented end-to-end (see Today JTBD funnels); the cache-first path makes the app usable offline; and the auth guard is target-aware so a no-household user lands on setup without looping (a bug the POC's flat guard hit). See the auth/onboarding spec under Specs & Plans.