Skip to main content

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.