v4.67 — Streak Insurance for Supporters
A Supporter whose habit streak broke yesterday can restore it from the card the app already shows them, at no XP cost, once a month — and a non-Supporter loses nothing they have today.
Summary
Roadmap item 48, the last open item in Phase 7 (Monetization & Teams), read as "add Supporter gating to the existing streak-freeze mutation." Planning found that premise wrong on both counts. GoalTracking::UseStreakFreeze has been proactive-only since v4.57 — it structurally cannot recover a streak that already broke, so gating it would gate the wrong feature. And recovering a broken streak was already shipped, twice, for free: Gamification::RepairStreak's once-per-goal-lifetime XP lane, and AcceptStreakMercy's once-ever free lane. The real ask was a third recovery lane — no XP cost, repeatable on an allowance, reserved to Supporters — and every free lane needed to stay exactly as it was, since taking one behind a paywall would be a dark pattern and a PBC-floor violation.
Eight phases across three domains shipped that lane. The backend (Phases 1, 2, 3b) added Gamification::ClaimStreakInsurance, reusing Gamification::StreakGapChecker and the existing 48-hour repair window rather than writing new streak math, and resolving the insurance offer independently of the free XP-lane offer so a goal that already spent its lifetime repair still surfaces an insurance option. Desi's UI-SPEC (Phase 3) ruled the card Dashboard-only and narrowed the upgrade prompt to exactly one state, catching a plan that would otherwise have summoned an upsell-only card for a user with no free option left. The frontend (Phases 4a, 4b, 4c) built the card, the upgrade moment, and — after a two-phase gap where the actual Dashboard wiring was never done despite a task package asserting it was — the live call site itself. Docs (Phase 5) closed with a human-escalated decision: streak insurance is the first Supporter perk that does something, which made docs/features/supporter.md's "purely cosmetic" framing false. Josh ruled the standing PBC commitment is "no features are ever paywalled," not "purely cosmetic" — the doc was corrected to that testable claim, and the milestone continued.
Ships behind streak_insurance_enabled at 0% rollout — this is complete build-out, not a launch. No user can reach the perk today.
Goal
A Supporter whose habit streak broke yesterday can restore it from the card the app already shows them, at no XP cost, once a month — and a non-Supporter looking at the same card loses nothing they have today and is told about the perk once, quietly, only at the moment it would actually have helped them.
Scope — What Shipped
Gamification::ClaimStreakInsurance(Interaction::Base) — Supporter gate, per-break guard, monthly allowance, reusesGamification::StreakGapCheckerandGoalTracking::UseStreakFreeze(system_granted: true)for the backfill. Never touchesstreak_repaired_count— a claim can never consume the free XP-repair allowance.streak_insurance_claimstable — unique index on(user_id, period_key), race-safe allowance enforcement;period_keycomputed in the user's own timezone.claimStreakInsuranceGraphQL mutation and eight newstreakRepairOfferfields (userIsSupporter,insuranceEligible,insuranceClaimsRemaining,insurancePeriodResetsAt,insuranceGoalPublicId,insuranceGoalName,insurancePreBreakHabitStreak,insuranceEligibleUntil), resolved from an independent target lookup so the free XP-lane offer is never disturbed.StreakRepairCard.vue— all eight UI-SPEC states, Storybook coverage per state; the free XP option is never hidden, de-emphasised, or shown below an upgrade prompt.RepairConfirmationModal.vuegains avariant: 'xp' | 'insurance'prop — the insurance claim confirms (unlike the XP lane's renewable-resource skip) because the monthly allowance is scarce and irreversible.SupporterUpgradeModal.vuegains a fourth perk row and corrected footer copy — no string anywhere still calls Supporter perks "cosmetic."Dashboard.vuewired end-to-end in Phase 4c: the insurance props, the modal mount, and the emit listeners that the first two frontend phases had built but never connected.streak_insurance_enabled— PostHog flag, fail-closed, registered client- and server-side, created at 0% in the same PR that introduced it.docs/operations/rollouts/v4.67-streak-insurance.md— new ramp plan and kill-switch runbook.docs/features/supporter.mdrewritten per Josh's PBC ruling;docs/development/gotchas.md's native-copy contract widened to cover functional perks.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | The entitlement and the claim | Shipped | 1 | ClaimStreakInsurance + streak_insurance_claims; streak_repaired_count deliberately untouched (D5) |
| 2 | GraphQL surface | Shipped | 1 | claimStreakInsurance mutation + eight offer fields; D8's independent two-target lookup |
| 3 | UI-SPEC — insurance lane and upgrade moment | Shipped | 1 | Eight-state design contract (Desi); Ruling 3 narrowed the upgrade line to one state on anti-dark-pattern grounds |
| 3b | Complete the insurance offer payload | Shipped | 1 | Closed S4's missing streak-number/expiry rendering gap |
| 4a | Build the insurance lane | Shipped | 1 | Card built for all eight states; flag registered at 0% |
| 4b | The upgrade moment and the checkout return path | Shipped | 1 | Perk row + corrected footers; Roy caught a MAJOR scoping bug (offer-level vs. surface-level predicate) |
| 4c | Wire the live call site | Shipped | 1 | Fixed the gap where Dashboard.vue never actually mounted the wiring the first two frontend phases built |
| 5 | Docs reconciliation and roadmap ledger | Shipped | 1 | Josh's PBC ruling on "purely cosmetic"; item 54 correctly left open against a stale ROADMAP instruction |
Key Decisions
- D1 —
UseStreakFreezeis not gated and not modified. Proactive freezes stay free for everyone; gating them would gate the wrong feature and paywall something already free. - D2 — Streak Insurance is a new lane on existing machinery. No new streak-gap math —
StreakGapCheckerandUseStreakFreeze(system_granted: true)are reused verbatim, the same discipline OBJ-3040 established. - D4 — The allowance is one claim per calendar month, for every Supporter tier. "Per billing period" doesn't survive contact with the tier table — a yearly Supporter's period is a year, and lifetime has none at all.
- D5 — A claim must never consume the free lane's allowance.
RepairStreak's guard is a lifetime counter; if a claim incremented it, paying would remove free value. The claim clears the repair window and backfills the gap instead, leavingstreak_repaired_countuntouched. - D8 — The offer resolver needs two independent target lookups, not one widened scope. Widening the existing XP-lane scope would change which goal a non-Supporter sees an offer for — a real regression. Two independent lookups resolve to the same goal in the common case and diverge safely otherwise.
- Ruling 3 (Phase 3, Desi) — the upgrade line appears in exactly one state, not two. The original plan would have summoned a card, for the first time, for a user with no free option left — a textbook upsell-only terminal state.
- The confirm step stays for the insurance claim (Phase 4c). The monthly allowance is scarce and irreversible, unlike the XP lane's renewable resource — spending it deserves a confirmation the XP lane's own design choices don't require.
- "No features are ever paywalled," not "purely cosmetic" (Phase 5, Josh's ruling). Streak insurance is the first Supporter perk that does something; the standing PBC commitment survives as a testable claim — a perk is never required, and no free capability is ever removed or degraded to sell it back.
Requirements Coverage
50 / 50 requirements satisfied (from v4.67-streak-insurance-for-supporters-MILESTONE-AUDIT.md).
| Phase | Count | Status |
|---|---|---|
| 1 — Entitlement and claim | 7 | All satisfied |
| 2 — GraphQL surface | 8 | All satisfied |
| 3 — UI-SPEC | 4 | All satisfied |
| 3b — Complete the offer payload | 4 | All satisfied |
| 4a — Build the insurance lane | 7 | All satisfied |
| 4b — Upgrade moment + checkout return | 7 | All satisfied |
| 4c — Wire the live call site | 8 | All satisfied |
| 5 — Docs reconciliation | 5 | All satisfied (item 54 satisfied by correction, not by the ROADMAP's own stale instruction — see the audit's Deviations section) |
Outcomes
Every free streak-recovery path — proactive freezes, XP repair, streak mercy — is provably unmodified: none of use_streak_freeze.rb, repair_streak.rb, or accept_streak_mercy.rb were touched by any phase-carrying commit in this milestone. A Supporter now has a fully-wired, source-verified path to restore a broken streak for free, once a month, indistinguishable in mechanism from the free XP lane beside it. docs/features/supporter.md now makes a testable commitment instead of a claim this milestone made false.
Nothing is reachable by a real user yet. streak_insurance_enabled sits at 0% rollout. The next step is a ramp decision, not more build work — see the rollout runbook for the plan and kill-switch procedure.
Tech Debt
- (Phase 5, OBJ-4058)
docs/product/user-guide.md§15 still carries the pre-ruling "purely cosmetic" framing — parked in backlog, gated on the flag ramping above 0%, same as the deferred user-facing changelog entry. - (ROADMAP § Open questions 2) The success-metric PostHog funnel ("30%+ of Supporters use streak insurance at least once") doesn't exist yet. Events are named and emitted; the dashboard itself is not built.
- (pre-existing, Keystone E35)
gamification.streak_repairedhas no confirmed subscriber, now shared by two publishers instead of one — this milestone documented the gap rather than papering over it, per its own instruction not to assert a subscriber that doesn't exist.
Related Artifacts
- Roadmap: v4.67-streak-insurance-for-supporters-ROADMAP.md
- Milestone Audit: v4.67-streak-insurance-for-supporters-MILESTONE-AUDIT.md
- Rollout runbook: docs/operations/rollouts/v4.67-streak-insurance.md
- Feature docs: docs/features/habits.md § Streak Repair and Streak Insurance, docs/features/supporter.md § Streak Insurance
- Source issue: OBJ-1134 ("Milestone: Phase 7 — Monetization Remainder"),
docs/product/roadmap.md§ Phase 7, item 48
Related Commits
5ed7abed— Phase 1: entitlement + claim (PR #3370)93e0a77b— Phase 2: GraphQL surface (PR #3375)8612d2f7— Phase 3: UI-SPEC (PR #3376)2acf3d86— Phase 3b: complete the offer payload (PR #3379)7085c797— Phase 4a: build the insurance lane (PR #3381)a352b5e06— Phase 4b: upgrade moment + checkout return (PR #3386)b6826472e— Phase 4c: wire the live call site (PR #3402)27d7606b— Phase 5: docs reconciliation + roadmap ledger (PR #3406)
Last updated: 2026-09-20 Version: v4.9.0 (this milestone ships behind a 0%-rollout flag; no version tag yet)