Skip to main content

Companion Creature ("the doubling") — Design Spec

Date: 2026-07-18 · Status: Approved (design) · Branch: feat/mvp1-personas-authz Type: Feature design → hands off to an implementation plan.

1. Summary

A per-child virtual companion that lives ambiently in the horizon scene each kid already sees. It is the emotional skin on the chore loop: it reacts when a kid finishes a chore, grows as they keep going, and can be fed (with its own currency) and dressed up. It is forgiving — it never decays, gets sick, dies, or guilt-trips. This is the "doubling" the owner has been describing: a Finch-like character.

Explicit strategic frame (decision C): the companion ships as a motivator in v1 but is architected to become a co-regulation partner — the thing that, in a later release, invites a kid to take a breather on a big task and models calm during an "unwind" step. That co-regulation layer is not in v1 (it is gated on the future unwind-items feature), but the v1 design must not foreclose it.

2. Goals & non-goals

Goals (v1 — "Tier 1: the companion, whole and small"):

  • One creature per kid, single species, ~3 behavior-gated growth stages.
  • Ambient in the horizon scene; tap opens a focused sheet.
  • Reacts to completions off the existing celebration signal.
  • Its own separate soft currency ("dewdrops"), earned from completions, spent only on a small cosmetics set. Never convertible to tokens.
  • Forgiving presence (no decay/death/guilt); a gentle, capped, optional "cozy-messy room" as the only neglect signal — on the environment, never on the creature.
  • Simple-art-first (DS visual language, few states) — not an elaborate rig.

Non-goals (explicitly OUT of v1, named so scope cannot creep):

  • Horizon Visitors / anti-novelty-cliff system (Tier 2).
  • Co-regulation layer — invites, breathing, unwind presence (Tier 3, gated on the unwind-items feature).
  • Rich Rive rig, large cosmetic catalogs, multiple species.
  • Parent-configurable creature settings (parents are not tuning creature hats).
  • Any dewdrops↔token convertibility — permanently out. That separation is the moat.

3. Load-bearing decisions (converged with the owner)

#DecisionRationale
CMotivator in v1, architected to become co-regulation partnerShips on the rails already built; co-regulation is the deliberate next layer, not a v1 rewrite. "Finish one deep thing."
Per-childOne companion per kid (not one shared household creature)It is the child's companion; attachment is personal.
Separate economyDewdrops: own bucket, earned from completions, spent only on cosmetics, never convertibleToken-fade ADR: the two economies must never cannibalize. Physical separation enforces it. Dewdrops keep giving feedback even as tokens deliberately fade → supports the fade-to-intrinsic moat.
Behavior-gated growthBig growth milestones are unlocked by real completions, not purchasableNo kid buys their way to a maxed creature and hollows it out.
Forgiving presenceNo decay, no death, no sickness, no guilt; growth pauses and resumes, never regressesThe ND-affirming thesis + the non-punitive principle that also governs unwind. Finch's forgiveness is why Finch works. A creature that withers would contradict the co-regulation role we are building toward.
Neglect → the room, not the creatureA gentle, capped, optional "cozy-messy" room over a long absenceOn-theme (chore app → messy room is the story); Animal-Crossing-weeds pattern. Consequence on the environment, never the being, never the child.
Ambient homeLives in the horizon scene the kid already sees; tap-to-focus (no new tab)Separate tabs are where companion engagement dies (out of sight; an executive-function ask ND kids struggle with). Ambient presence is also what makes the future co-regulation "invite" possible. Keeps kid-mode's 4-tab shell intact.
Simple-art-firstDS visual language, few states; save the elaborate Rive rig for laterArt is the single biggest cost. Ship the bond, not the spectacle.

4. Architecture

One data path, exactly like every other feature:

CompanionBloc → CompanionRepository (cache-first)
→ Client facade → CompanionService (domain rules)
→ Adapter (Drift local + cloud PostgREST)

4.1 Projected vs stored

The engagement features already shipped (badges/streaks/timeline) are pure projections — zero storage. The companion cannot be fully projected because owning/equipping cosmetics and spending currency are genuine mutable state. The split:

PieceSourceStored?
Dewdrops earnedprojection from ChoreCompletion rows (N per completion)no — derived
Dewdrops balanceearned (projected) − spent (stored), zero-flooredfold, like tokens
Growth stagepure function of lifetime completions (behavior-gated)no — derived
Cosmetics owneddistinct entries in the spend ledgerderived from ledger
Cosmetics equipped + creature namegenuine mutable per-kid stateyes
Room cozy-messinessprojection from days-since-last-completion (capped) + intensity settingno — derived

4.2 Schema — two new tables (first real engagement-track migration)

  • member_companion — one row per kid: member_id (PK/FK), species (fixed single value in v1), name (nullable, kid-set), equipped (cosmetic ids), created_at.
  • companion_ledgerappend-only, physically separate from ledger_entries. One row per cosmetic purchase: member_id, cosmetic_id, dewdrops_cost, ts. Owned = distinct cosmetic_ids; balance = earned − sum(dewdrops_cost).

Why a separate ledger, not a new LedgerEntryKind: the token-fade ADR's spirit is that the two economies must never cannibalize each other. Physical table separation enforces it — dewdrops cannot contaminate the token-balance fold even by accident.

Forward-compat for Tiers 2/3: do not pre-add speculative columns for visitors or co-regulation (that is the pre-build-the-platform trap the sovereign ADR warned against). "Slots in" means clean boundaries: CompanionService computes growth from a small policy object, and later tiers add their own tables/services against the same facade. Zero speculative schema now.

4.3 Critical correctness carry-over (the A3/A4 blind-spot)

Dewdrops must be earned from ChoreCompletion rows via the existing Client.getCompletions({memberId}) facade — not from ledger earns. Expectations pay zero tokens and write no ledger entry, so a kid on expectation-only chores would earn zero dewdrops if we sourced from the token ledger. Sourcing from completions means every finished chore feeds the creature, token or not. This is the same trap that bit badges (A3) and streaks (A4).

4.4 Dual gate + RLS

A child can read and mutate only their own companion (spend, name, equip); parents in the household may read a kid's companion, but the kid owns it (no parent write to a kid's creature in v1). Service authz AND RLS, per the house rule. The two-identity live proof reuses the request.jwt.claims SQL-smoke technique (the headless publishable key can't attach a JWT).

5. Domain rules (CompanionService)

All logic lives in the SDK service behind the facade — the app never computes economy or growth itself.

  • Earn rule: each ChoreCompletion grants kDewdropsPerCompletion (named constant, default 1). Earned = completions × rate. There is no explicit "feed" action — the creature is fed passively by doing chores.
  • Growth stages (behavior-gated): pure function of lifetime completions against a small policy object. Default thresholds: stage 1 from start, stage 2 at 10 lifetime completions, stage 3 at 30 (all tunable constants — the plan may adjust, but must ship concrete values, not placeholders). Growth only advances; it never regresses (forgiving-presence enforced in code, not just art). Cosmetics are dewdrops; growth is earned completions — you cannot buy your way up.
  • Purchase validation (the one mutating economy op), three typed failures mapped to dialogs (house pattern):
    • insufficientDewdrops — balance < cost;
    • alreadyOwned — cosmetic already in the ledger (idempotent, no double-charge);
    • unknownCosmetic — id not in the static catalog. Balance is zero-floored at the fold (mirrors the token zero-floor) — a race or replay can never drive dewdrops negative.
  • Equip / rename: pure state writes on member_companion, validated — you can only equip a cosmetic you own (notOwned otherwise); name is length-capped and profanity-light-checked (reuse existing validation).
  • Cosmetics catalog: a small static catalog in config (like the rewards catalog shape), fixed set with dewdrop prices. No per-household config in v1.
  • Cozy-messiness: read-only projection — capped function of days-idle + the intensity setting (full / subtle / off); no failure modes.
  • Dual-gate failures: a child touching another kid's companion fails at both service (actor-scoped) and RLS (self-scoped policy) — never a silent cross-member write.

6. Rendering, presence & reactive wiring

  • Home = the horizon scene. Evolve DsHorizonHouse/the horizon backdrop into a small layered composition: a creature layer (idle-animating in its spot) and a room-tidiness layer (cozy-messy decorations that fade in over a long absence, clear on return), painted in the DS's existing visual language.
  • Tap-to-focus opens a DsCompanionSheet (not a tab): creature larger, name (tap to rename), current growth stage, dewdrops balance, a small cosmetics tray to buy/equip. That is the whole surface.
  • Wired into the existing loop: the creature reacts to the same ChoreDoneState transition the celebration overlay fires on. On a completion: a happy bounce alongside the celebration burst + a small dewdrops "+N" float, visually distinct from the token "+N" (different color/glyph) so the two economies read as separate at a glance. No new trigger plumbing.
  • Reduced-motion: under MediaQuery.disableAnimations, the creature settles to a static happy pose (no idle loop, no bounce) via MotionTokens.durationOrZero. The companion is never a motion problem.
  • Cozy-messiness rendering: projection in gentle tiers (tidy → a few leaves → cozy-dusty), hard-capped (two months looks no worse than one), gated by the intensity setting; any completion visibly steps it back toward tidy.
  • Kid-mode fit: no new tab — lives in the home/today backdrop kids already land on; the focused sheet is reachable only by tapping the creature.

7. Testing

SDK service unit tests (pure, fast):

  • earn projection: N completions → correct dewdrops; expectation-only kid still earns (the A3/A4 guard — asserts source is ChoreCompletion, not the token ledger);
  • zero-floor: spend cannot go negative; double-purchase is idempotent (alreadyOwned, no double-charge);
  • growth-stage boundaries: exact thresholds + the never-regresses invariant (feed backwards → stage holds);
  • purchase failures: insufficientDewdrops / alreadyOwned / unknownCosmetic; equip-not-owned → notOwned;
  • cozy-messiness: tier boundaries, the two-months-caps-at-one-month invariant, off → always tidy.

Flow test (loop end-to-end): complete a chore → dewdrops increment and creature reacts; buy a cosmetic → balance decrements, owned + equippable; rename persists.

Two-identity cloud smoke (RLS proof, request.jwt.claims): child mutates own companion; child cannot read/write a sibling's; parent can read a kid's companion within household.

Widget/golden: creature ambient in the scene; the focused sheet; the three cozy-messy tiers; reduced-motion static pose.

Baselines: app 553 / SDK 1049 must not drop — new tests add on top.

8. File map

New:

  • Migration: member_companion + companion_ledger (+ Drift mirror, cloud codec).
  • SDK: CompanionService + models + typed exceptions; Client facade passthroughs (getCompanion, purchaseCosmetic, equip, rename); the static cosmetics catalog.
  • App: CompanionRepository (cache-first); CompanionBloc.
  • Design system: creature + room-tidiness layers evolving DsHorizonHouse; DsCompanionSheet; the dewdrops "+N" float.

Modified:

  • The horizon backdrop host (to mount the creature + tidiness layers).
  • The completion/celebration wiring site (to fire the creature reaction + "+N").

9. Global constraints (inherited by every implementation task)

  • Dart/Flutter monorepo, FVM (fvm flutter / fvm dart, never node).
  • Supabase project bgedvvmihygwxhjxlvfu, anon/publishable key only in the app; service-role only inside Edge Functions; migrations file-only, applied live by the controller after review.
  • ONE data path (Bloc → Repository → Client facade → Service → Adapter; domain rules in the SDK Service).
  • DUAL GATE (service authz AND RLS) for every privileged step; append-only ledger; zero-floored balance.
  • Dewdrops sourced from ChoreCompletion (never the token ledger); dewdrops never convertible to tokens.
  • Reduced-motion honored via MotionTokens.durationOrZero.
  • Explicit git add (never -A); never stage graphify-out/, .superpowers/, .claude/. Graphify-first for codebase questions; graphify update . after code changes.
  • Suite baselines app 553 / SDK 1049 must not drop.

10. Sequencing note

Per the "deeper not wider" positioning and the Picniic guardrail, this is one deep feature to finish whole before opening the next front (unwind items → then native calendar). Tiers 2 (Horizon Visitors) and 3 (co-regulation) are deferred by design and depend, respectively, on proven retention and on the unwind-items feature existing.