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-onlyLedgerService, the zero-floor debit guard, and theApprovalServiceearn path are already built; this epic generalizes them into envelopes and adds the one new verb — move funds.
| Type | Functional |
| Layer (build approach) | Data — ledger reshape + RLS + migration |
| RICE | R 9 × I 3 × C 80% / E 6 = 3.6 · Tier MVP-1 |
| KPI (summary) | Funds-moved/active member/week; goal-envelope allocation % |
| Traces to | feature money · C4 code |
| Depends on | Personas / 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_movedevents per active member per week, and the share of active members with ≥ 1goal_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.
| Event | When | Key properties (coarse) | Feeds |
|---|---|---|---|
funds_moved | a move commits (ledger txn) | fromKind, toKind, amountBucket, actorRole (member|admin) | primary KPI → funds-moved/member/week |
goal_allocated | move whose destination is a goal envelope | amountBucket, progressBandAfter | goal-allocation rate (KPI numerator) |
goal_reached | goal envelope balance ≥ target | daysToReach (coarse) | completion / motivation signal |
bucket_created | member or admin creates a non-permanent bucket | kind, byRole | envelope-budgeting adoption |
P1-with-surface. These four events are P1 but ship with their surface —
funds_moved+goal_allocatedland 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+ creditto, summing to zero (no tokens created/destroyed) — both balances re-project, andfunds_movedfires. - 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.
Acceptance — Given 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.
Acceptance — Given 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:10When 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
titheflag.
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
permanentflag — 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.
Acceptance — Given 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.
Acceptance — Given 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. Acceptance — Given 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).
Acceptance — Given the migration When it runs Then Household.address (optional) exists with a setter; Place.address is dropped/ignored.
Architecture considerations
- One data path —
MoneyBloc → Repository → client_sdkfacade →LedgerService/EconomyService→ Adapter; no directdrift/supabasein 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
LedgerServiceand 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)
moveFundsservice + facade method; (2) envelope-id tag generalization (ledger tag from fixedBucket→ 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
permanentbuckets (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 cashout —
currencyPerTokenis 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.