Skip to main content

Money & Envelopes — requirements

Epic. MVP-1 requirements breakdown for the envelope money model (see the feature architecture). Each FR is sized to become one or a few user stories. The base wallet (walletOf), the append-only LedgerService, the zero-floor debit guard, and the ApprovalService earn path are already built; this epic generalizes them into envelopes and adds the one new verb — move funds.

TypeFunctional
Layer (build approach)Data — ledger reshape + RLS + migration
RICER 9 × I 3 × C 80% / E 6 = 3.6 · Tier MVP-1
KPI (summary)Funds-moved/active member/week; goal-envelope allocation %
Traces tofeature money · C4 code
Depends onPersonas / authz · Today (effectiveTokens earn source) · Member Profile (envelopes + goals live here) · Offline sync · Security

Success criteria (definitive KPI)

Success = ≥ 40% of active members move funds into ≥ 1 goal envelope within 3 weeks of their first earning, sustained across weeks 2–4 post first-earn.

  • Metric: funds_moved events per active member per week, and the share of active members with ≥ 1 goal_allocated (a move whose destination is a goal envelope).
  • Target: ≥ 40% of active members allocate to a goal envelope and ≥ 1.0 funds_moved/active member/week.
  • Window: weeks 2–4 after the member's first earning (first credit to Unallocated).
  • Why this bar: deliberate allocation — moving money out of Unallocated and into a named envelope — is the envelope-budgeting intent made visible. It is the money-side expression of intentional, neurodiversity-affirming money.
  • Guardrail (token-fade): the goal-envelope allocation rate should hold or rise over 8 weeks while median tokens-per-completion trends down — the intrinsic-motivation thesis. Allocation falling as tokens fade is a regression, not success (members only moved money because tokens were plentiful).
  • Baseline: no install base yet — treat 40% / 1.0-move as the launch hypothesis, confirm with the first cohort, then re-baseline from wk-2 data.

Analytics — events to record

Internal-BI bucket only — content-free, no child identity, never marketing (see Privacy model under Security). All member references are opaque/hashed; amounts are coarse buckets, never exact balances. Emitted through the consent-gated analytics substrate.

EventWhenKey properties (coarse)Feeds
funds_moveda move commits (ledger txn)fromKind, toKind, amountBucket, actorRole (member|admin)primary KPI → funds-moved/member/week
goal_allocatedmove whose destination is a goal envelopeamountBucket, progressBandAftergoal-allocation rate (KPI numerator)
goal_reachedgoal envelope balance ≥ targetdaysToReach (coarse)completion / motivation signal
bucket_createdmember or admin creates a non-permanent bucketkind, byRoleenvelope-budgeting adoption

P1-with-surface. These four events are P1 but ship with their surfacefunds_moved + goal_allocated land with the move-funds sheet (FR-MONEY-1), not after; the KPI is only measurable once both are instrumented.

Scope

This is a domain reshape of the existing bucket+ledger model into envelopes. MVP-1 delivers the one verb — move funds → envelope — implemented as a single zero-summing ledger transaction with the zero-floor guard; the Unallocated earn-landing envelope; and the envelope generalization of the ledger tag so goal envelopes carry their own balance. Goal-envelope balances and the optional household default-allocation may stage into MVP-1.x. Multi-currency, real-money cashout, and scheduled auto-moves are not MVP-1.

Functional requirements

FR-MONEY-1 — Move funds → envelope (the one verb)

Priority: P1 · Status: 🔨 to build — moveFunds service + facade absent today (transferTokens does not exist). Statement. As a member (or admin), I move funds from one envelope to another so my money is deliberately allocated, not auto-split. Acceptance

  • Given source balance ≥ amount When I move from→to Then one ledger transaction is appended — debit from + credit to, summing to zero (no tokens created/destroyed) — both balances re-project, and funds_moved fires.
  • Exposed as moveFunds({from, to, amount}) on the SDK facade → LedgerService.

FR-MONEY-2 — Zero-floor guard on the debit (service AND schema)

Priority: P1 · Status: 🔨 to build — reuses the redeem-path guard. Statement. A move can never drive an envelope below zero. AcceptanceGiven source balance < amount When I request the move Then it is rejected (InsufficientBalanceException) and no ledger entry is written. Enforced in the service (debit throws) and in schema (the SQL zero-floor trigger), exactly as redeem is today.

FR-MONEY-3 — Envelope generalization (ledger tag = envelope id)

Priority: P1 · Status: 🔨 to build — foundational; reshapes the ledger tag. Statement. The ledger tag generalizes from the fixed Bucket (spend/save/give) to an envelope id, so goal envelopes carry their own balance. Acceptance

  • Given a goal envelope with its own id When balances project Then its balance comes from entries tagged with its id — not derived from the Save bucket.
  • Given existing spend/save/give buckets When the migration runs Then they become envelopes with stable ids and unchanged balances.

FR-MONEY-4 — Earnings credit Unallocated by default

Priority: P1 · Status: 🔨 to build — today earnings auto-split via EarningsSplit. Statement. As a member, my earnings land in Unallocated so the family allocates deliberately, instead of money being auto-split out of sight. AcceptanceGiven no household default allocation When a chore is approved Then the ApprovalService earn path (unchanged) credits 100% to the Unallocated envelope; only the destination changes.

FR-MONEY-5 — Optional household default allocation

Priority: P2 · Status: 🧊 may stage (MVP-1.x) — reframes EarningsSplit as an optional default. Statement. As an admin, I may configure a default routing (e.g. 10% → Give for tithing); with none set, all stays Unallocated. Acceptance

  • Given a route give:10 When a chore is approved Then 10% auto-moves to Give via ledger moves; the remainder lands Unallocated.
  • Given no route configured When approved Then 100% lands Unallocated (FR-MONEY-4). Pairs with the tithe flag.

FR-MONEY-6 — Default buckets = admin template (permanent flag)

Priority: P1 · Status: 🔨 to build. Statement. As an admin, I define the starter buckets every new member begins with; some are permanent and the member cannot remove them. Acceptance

  • Given the default template Then Spend + Save are permanent; Give is present but admin-removable / renamable / re-iconable.
  • Given a new member When their wallet is created Then the template's buckets — incl. the permanent flag — are copied onto their starter buckets.

FR-MONEY-7 — Member CRUDs their own non-permanent buckets

Priority: P2 · Status: 🔨 to build. Statement. As a member, I add / rename / delete my own buckets (my budget), except the admin-permanent ones. AcceptanceGiven a non-permanent bucket I own When I delete it Then it is removed (balance must first be moved out; zero-floor applies); Given a permanent bucket When I try to delete it Then blocked. bucket_created fires on add.

FR-MONEY-8 — Goal envelope balance + "reached when balance ≥ target"

Priority: P2 · Status: 🧊 may stage (MVP-1.x). Statement. As a member, a goal envelope shows progress and is "reached" when full. AcceptanceGiven a goal target When moves fill it Then it shows balance / target progress; at balance ≥ target it is reached and goal_reached fires. Goal lifecycle is unchanged (Member Profile).

FR-MONEY-9 — Move-funds authorization (member / admin / helper)

Priority: P1 · Status: 🔨 to build — DECIDED in feature open-decisions. Statement. Only the right roles can move funds. Acceptance

  • Given I am the member Then allowed only among my own envelopes.
  • Given I am an admin Then allowed on any member's wallet.
  • Given I am a helper (babysitter) Then denied — helpers only approve/deny jobs (Personas / authz).

FR-MONEY-10 — Supervised-bounty attribution fork

Priority: P1 · Status: 🔨 to build — new; the COPPA-safe earn split. Statement. A child bounty's earnings credit the right envelope depending on whether the child has their own consented account. Acceptance

  • Given a bounty completed via the supervised path (no per-child record) When approved Then earnings credit a household-attributed envelope, not a child wallet (no minor record created).
  • Given a consented child with their own account When approved Then earnings credit their own Unallocated envelope. Supervised-path attribution is household-scoped only (see Privacy model).

FR-MONEY-11 — Family budget read-only on Household

Priority: P2 · Status: 🔨 to build. Statement. As a family, the household budget is set in Admin and shown read-only on the Household overview. AcceptanceGiven a family budget set in Admin When I open Household Then I see a budget summary + goal summaries (family + per-member) — overview only, no editing. Family-scope envelopes (incl. family goals) hang off the household.

FR-MONEY-12 — Household.address migration (off Place)

Priority: P2 · Status: 🔨 to build — part of the migration. Statement. The house address belongs to the Household, not a room (Place). AcceptanceGiven the migration When it runs Then Household.address (optional) exists with a setter; Place.address is dropped/ignored.

Architecture considerations

  • One data pathMoneyBloc → Repository → client_sdk facade → LedgerService / EconomyService → Adapter; no direct drift/supabase in presentation.
  • A move is one ledger transaction — debit source + credit dest, summing to zero; append-only ledger invariant holds (generalization, not a new subsystem). No tokens are created or destroyed.
  • Zero-floor in service AND schema — the debit throws in LedgerService and the SQL trigger rejects sub-zero balances; both must cover the new move path.
  • Household-scoped RLS — a move never touches another household's envelopes; the cloud adapter relies on household-scoped policies (admin-on-any-wallet is a role check inside the household scope, never cross-household).
  • Offline-first — moves must commit fully offline (Drift write-through cache), queue, and sync later (Offline sync); a synced move must be idempotent — no double-debit on the offline→online transition.
  • SDK gaps to build: (1) moveFunds service + facade method; (2) envelope-id tag generalization (ledger tag from fixed Bucket → envelope id); (3) the Unallocated envelope + earn-to-unallocated default; (4) optional household default allocation + earn-time auto-move; (5) Household.address + setter; (6) goal-envelope balance (reached when balance ≥ target).
  • Privacy model (three buckets). OPERATIONAL — child wallet/goals → VPC, never leaves the privileged path. INTERNAL BI — content-free, no child identity, never used for marketing (the analytics table above). MARKETING — parent/account-derived only (account count, plan type, parent-feature-active); never child-derived. Never market to children. The supervised-bounty fork (FR-MONEY-10) keeps minor data out of the operational store entirely.

Design work (ahead of build)

  • Move-funds sheet — from→to envelope picker + amount entry with inline zero-floor feedback (block + explain when amount > source balance); confirm shows the resulting two balances.
  • Envelope vs goal visuals — plain envelope shows a balance; goal envelope shows a progress bar (balance / target) and a "reached" state.
  • Default-bucket editor (Admin) — manage the starter template; lock affordance for permanent buckets (Spend/Save); Give editable/removable.
  • Unallocated affordance — a "money waiting to be allocated" surface that nudges deliberate allocation (the KPI behavior) without nagging.
  • Family budget summary (Household) — read-only budget + goal rollups.

Decisions (resolved for MVP-1)

Resolved — see the MVP-1 decisions log for the canonical record, rationale, and status legend (✅ decided · ⚖️ counsel confirms · 🔜 MVP-1.x).

  • D-MONEY-1 — Scope & sequencing. ✅ Money is its own sub-project (like SP3), but its full surface ships in MVP-1, not staged out. Household surfaces read-only money tiles that the Money sub-project's writes feed.
  • D-MONEY-2 — Goal balances & default allocation. ✅ Both FR-MONEY-8 (goal-envelope balances) and FR-MONEY-5 (household default allocation) ship in MVP-1 (full-moves anchor).
  • D-MONEY-3 — Supervised-attribution visibility (FR-MONEY-10). ✅ A household-attributed envelope surfaces on Household as a "Family / Unassigned" envelope tile, never under a child's name or wallet. When no per-child record exists (supervised, no VPC), the bounty credits the household envelope; the tile reads as household-owned. (Ties to the money fork in the privacy-compliance model.)
  • D-MONEY-4 — Default-allocation UX.Percentage routes (a parent sets % splits across envelopes/goals), not fixed amounts — reads as a deliberate "where earnings flow" config. Fixed-amount routing deferred (YAGNI).

Out of scope (MVP-1)

  • Multi-currency / real-money cashoutcurrencyPerToken is display only.
  • Scheduled / recurring auto-moves beyond the earn-time default allocation.
  • Month-keyed budget categories — "budget" here is the member's envelope set, not a calendar-bucketed category ledger.
  • Cross-member envelope sharing / pooled contributions to a shared goal.
  • Marketing use of any money data — barred by the privacy model, not deferred.