Money & Envelopes — feature architecture
BUILT for MVP-1 — General model (2026-07-02). Money S1–S3 shipped. The earn→General→move→spend loop is live. The full envelope/goal-envelope generalization (goal-envelope balances, household default allocation, named custom envelopes) is deferred — see §Open decisions below.
The original DRAFT proposed "Unallocated" as the earn-landing envelope name; the shipped name is General (user decision, ND-informed; HS-4 resolved).
What shipped (2026-07-02)
Money S1 — Bucket.general earn-landing (SDK + schema)
- New
Bucket.generalvalue added;_approveCompletionnow credits 100% togeneral(replacesEarningsSplit.allocateauto-split on the earn path). LedgerEntryKind.moveadded (distinct fromearn/spend/give/adjust).- Live migration: 4
BucketCHECK constraints widened + move kind whitelisted; zero-floor trigger made bucket-generic. EarningsSplitretained as move-sheet suggestion defaults (not a hard earn-route).
Money S2 — moveFunds verb (SDK)
LedgerService.moveFunds(from, to, amount, actingMemberId)— debit source + credit dest in one call; pair shares arefId; atomicity: validation/auth/zero-floor all pre-write (debit-first → mid-pair failure destroys-not-mints).- Conservation: pair sums to zero by construction (6-branch test matrix).
- Authz matrix: adult self ✓ / consented child ✓ / non-consented child ✗ / parental
on-behalf (child) ✓ /
otherAdultacting ✗. moveOwnFundsgating on facade.
Money S3 — Wallet General row + Move sheet + Undo (app UI)
- General row first in wallet (coin token icon + helper text).
- Move sheet: amount entry (prefills from
EarningsSplitsuggestion defaults) → instant move; no confirmation dialog (reversible, ND deliberateness); Undo snackbar (compensating move, catalog nonce pattern). - Spend keeps tap-and-confirm (irreversible → the one meaningful gate).
DsTokenBucket.generaldesign token.
The model in one idea
Everything a member holds is an envelope. An envelope holds funds and has a display mode:
- plain envelope — Spend / Save / Give (+ "unallocated"): shows a balance.
- goal envelope — an envelope with a
target: shows progress and is "reached" when full. A goal is just an envelope that displays differently.
The one core verb everywhere is move funds → envelope.
A MEMBER'S FUNDS = ENVELOPES
┌ Unallocated ┐ ┌ Spend ┐ ┌ Give ┐ plain → balance
│ 5 │ │ 30 │ │ 3 │
└─────────────┘ └───────┘ └──────┘
┌ Save ───────────────────────────────────────────────┐
│ ┌ 🎯 Skateboard ┐ ┌ 🎯 Lego set ┐ │ goal → progress
│ │ ▓▓▓░ 40 / 50 │ │ ▓░░░ 8 / 30 │ │ (target + bar)
│ └────────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────┘
⇄ move funds between ANY envelopes
Why it fits the architecture (no invariant break)
Balances are already projected from the append-only LedgerEntry stream
(spec invariant 2; zero-floor invariant 1). So:
- an envelope = ledger entries tagged with that envelope (today:
Bucketspend/save/give; this generalizes the tag to "envelope id", where goal envelopes have their own id). - a move = one ledger transaction: a debit of the source envelope + a
credit of the destination, summing to zero (no tokens created/destroyed). The
zero-floor check runs on the debit exactly as
redeemdoes today. - earning still flows through the single token-moving path
(
ApprovalService); it just credits the Unallocated envelope by default (see earn→allocate below) instead of auto-splitting.
So envelopes + moves are a generalization of the existing bucket+ledger model, not a new subsystem.
Earn → allocate flow
- Default = (a) unallocated. Earnings land in the member's Unallocated envelope; the family then deliberately allocates (the envelope-budgeting intent — visible, intentional money; good for the neurodiversity-affirming goal).
- Optional household default allocation. Parents may configure a default
routing (this reframes today's
EarningsSplitas an optional default, not a hard rule) — e.g. route 10% to Give to encourage tithing/altruism (pairs with the existingtithefeature flag). With no default set, 100% lands unallocated.
Move-funds state machine
Goal lifecycle (request/approve/complete/archive) is unchanged — see Member Profile; a goal envelope can be filled by moves and is "reached" when balance ≥ target.
ERD
address moves off Place (a room has no address) onto Household
(optional). Family-scope envelopes (incl. family goals) hang off the household.
SDK surface — shipped vs deferred
Shipped (2026-07-02):
walletOf(spend/save/give/general projection)moveFunds({from, to, amount, actingMemberId})— service + facade (S2)Bucket.generalearn-landing;LedgerEntryKind.moveEarningsSplitretained as suggestion defaults onlyupdateEconomyConfig(split/currencyPerToken/emoji)(unchanged)- The full goal lifecycle (
createGoal,updateGoal,completeGoal,archiveGoal) - Append-only
LedgerService+ zero-floor on debits ApprovalServiceearn path (creditsBucket.generalsince S1)
Deferred (not built for MVP-1):
- Envelope generalization — extend the ledger tag from fixed
Bucketto a generic envelope id so goal envelopes carry their own balance (today goal progress is derived from the save bucket, not its own envelope). - Household default allocation — optional earn-time auto-move config
(reframed
EarningsSplit); retained as move-sheet suggestion defaults for now. - Goal-envelope balance — "reached when balance ≥ target" display; goal progress still derived from save bucket in MVP-1.
Household.address— note: houses table was introduced andhouseholds.addresswas subsequently dropped entirely (house entity slice); address lives on thehousestable, notHouseholdmodel.- Member bucket CRUD (add/rename/delete custom envelopes) — not built.
- DF-1 addendum —
ledger_entries_insertRLS needs a self-move widening carve-out when helper/adult cloud identity ships (non-parental actors writing their own move rows).
Test plan (anchors)
- Ledger/unit: a move debits source + credits dest, sums to zero; zero-floor rejects an over-balance move; earn credits Unallocated (no default) / routes per default (with default).
- Bloc: wallet/envelope load; move request → success/rejected; goal envelope reaches target.
- Flow: allocate from Unallocated into a goal → goal progresses; tithe default routes a slice to Give on earn.
Buckets, budgets & default buckets
- Default buckets (admin template). The admin (Profile → Admin) defines the
starter buckets every new member begins with — e.g. Spend, Save (permanent),
Give. A bucket marked
permanentcannot be removed by the member. Replaces the POC "giving destination" — giving is just a default Give bucket, and encouraging tithing = a default-allocation route into it. - A member's budget = their own envelopes. A kid CRUDs their own buckets
(add / rename / delete) — except the admin-
permanentones. "Budget" here is the member's bucket set + how they allocate funds into it (the envelope plan), not a month-keyed category list. - Family budget. A household-level budget set in Admin and shown read-only on the Household overview.
- Summaries on Household. The Household tab rolls up a budget summary + goal summaries (family + per-member) — overview only, no editing.
ENVELOPE.permanent is set on the household default-bucket template and copied
onto each member's starter buckets; member bucket CRUD must respect it.
Open decisions (post-MVP-1 status)
- Who can move funds? ✅ DECIDED + BUILT. The member themselves (among their
own envelopes) and an admin (any member's wallet). Parental gates stay on earning
(chore approval) and spending (redemption); a helper can only approve/deny jobs —
never move funds. S2
authz matrixenforces this. - Base envelopes ✅ DECIDED + BUILT. Spend + Save are the fixed base; Give is addable/removable by admin. General is the earn-landing bucket (implicit, not user-removable). Member CRUD of custom buckets deferred.
- HS-4 (Unallocated vs auto-split) ✅ RESOLVED (2026-07-01). Earn lands in
General (not auto-split via
EarningsSplit).EarningsSplitis retained as move-sheet suggestion defaults (visual prompt, not a hard rule). Full envelope/goal-envelope generalization deferred to a future pass.
Relationship to other features
- Member Profile: a member's envelopes + goal envelopes + the move action live here (personal money).
- Household: family envelopes + Family Goals; the default allocation + house address are household config.