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)
| # | Decision | Rationale |
|---|---|---|
| C | Motivator in v1, architected to become co-regulation partner | Ships on the rails already built; co-regulation is the deliberate next layer, not a v1 rewrite. "Finish one deep thing." |
| Per-child | One companion per kid (not one shared household creature) | It is the child's companion; attachment is personal. |
| Separate economy | Dewdrops: own bucket, earned from completions, spent only on cosmetics, never convertible | Token-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 growth | Big growth milestones are unlocked by real completions, not purchasable | No kid buys their way to a maxed creature and hollows it out. |
| Forgiving presence | No decay, no death, no sickness, no guilt; growth pauses and resumes, never regresses | The 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 creature | A gentle, capped, optional "cozy-messy" room over a long absence | On-theme (chore app → messy room is the story); Animal-Crossing-weeds pattern. Consequence on the environment, never the being, never the child. |
| Ambient home | Lives 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-first | DS visual language, few states; save the elaborate Rive rig for later | Art 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:
| Piece | Source | Stored? |
|---|---|---|
| Dewdrops earned | projection from ChoreCompletion rows (N per completion) | no — derived |
| Dewdrops balance | earned (projected) − spent (stored), zero-floored | fold, like tokens |
| Growth stage | pure function of lifetime completions (behavior-gated) | no — derived |
| Cosmetics owned | distinct entries in the spend ledger | derived from ledger |
| Cosmetics equipped + creature name | genuine mutable per-kid state | yes |
| Room cozy-messiness | projection from days-since-last-completion (capped) + intensity setting | no — 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_ledger— append-only, physically separate fromledger_entries. One row per cosmetic purchase:member_id,cosmetic_id,dewdrops_cost,ts. Owned = distinctcosmetic_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
ChoreCompletiongrantskDewdropsPerCompletion(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 (notOwnedotherwise); 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
ChoreDoneStatetransition 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) viaMotionTokens.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;Clientfacade 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 stagegraphify-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.