Skip to main content

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.general value added; _approveCompletion now credits 100% to general (replaces EarningsSplit.allocate auto-split on the earn path).
  • LedgerEntryKind.move added (distinct from earn/spend/give/adjust).
  • Live migration: 4 Bucket CHECK constraints widened + move kind whitelisted; zero-floor trigger made bucket-generic.
  • EarningsSplit retained 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 a refId; 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) ✓ / otherAdult acting ✗.
  • moveOwnFunds gating 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 EarningsSplit suggestion 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.general design 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: Bucket spend/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 redeem does 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 EarningsSplit as an optional default, not a hard rule) — e.g. route 10% to Give to encourage tithing/altruism (pairs with the existing tithe feature 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.general earn-landing; LedgerEntryKind.move
  • EarningsSplit retained as suggestion defaults only
  • updateEconomyConfig(split/currencyPerToken/emoji) (unchanged)
  • The full goal lifecycle (createGoal, updateGoal, completeGoal, archiveGoal)
  • Append-only LedgerService + zero-floor on debits
  • ApprovalService earn path (credits Bucket.general since S1)

Deferred (not built for MVP-1):

  1. Envelope generalization — extend the ledger tag from fixed Bucket to a generic envelope id so goal envelopes carry their own balance (today goal progress is derived from the save bucket, not its own envelope).
  2. Household default allocation — optional earn-time auto-move config (reframed EarningsSplit); retained as move-sheet suggestion defaults for now.
  3. Goal-envelope balance — "reached when balance ≥ target" display; goal progress still derived from save bucket in MVP-1.
  4. Household.address — note: houses table was introduced and households.address was subsequently dropped entirely (house entity slice); address lives on the houses table, not Household model.
  5. Member bucket CRUD (add/rename/delete custom envelopes) — not built.
  6. DF-1 addendumledger_entries_insert RLS 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 permanent cannot 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-permanent ones. "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)

  1. 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 matrix enforces this.
  2. 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.
  3. HS-4 (Unallocated vs auto-split) ✅ RESOLVED (2026-07-01). Earn lands in General (not auto-split via EarningsSplit). EarningsSplit is 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.