Lissin Monetization — Master Report
2026-07-20 · the single source: status, self-review, everything done, what's left, and links to the deep-dive reports. Base branch = dev everywhere.
The backend entitlement + quota + enforcement engine is complete and hardened; grandfathering covers cohorts and hand-picked users; iOS contains the Phase-4 surface and Pro creator features; and admin contains the operating surfaces. The work is now integrated: backend #522 (+ #523/#524) into dev, admin #168 into dev, and iOS #317 into develop. It remains observe-only / capability-gated / flag-gated until configuration and rollout. On 21 July, the missing offer jump was traced to swipe dismissal bypassing RevenueCat’s callback plus standard-offering fallback masking absent discounts. Fix c6305160 is pushed on fix/offer-ladder-dismiss-pop and still needs merge. Product sign-offs, exact save50/save65 RevenueCat/App Store artifacts, sandbox QA, a new build, and flag ramp remain.
New this round, on top of the earlier foundation: backend #515 gained grandfather-by-user-IDs (preview + idempotent bulk apply) and a campaigns audit; admin #163 gained a Users-page multi-select → "Grandfather selected (N)" flow and a grandfathering audit page; iOS #311 is the offer-stage rescue ladder (persists & resumes, resets on purchase, commits a stage only after its offering loads); iOS #312 is Phase 4 — gates (transcript / playback-speed / soundscape / explore-time / file size+count), trial banner, usage nudges, and live balances. All new review P1s fixed: #515 require-filters (400 not 500) + past-expiry reject; #311 ladder concurrency (commit-after-load + re-entrancy guard). #312 was rebased onto the fixed ladder — the whole iOS stack builds & 356 tests pass. iOS merge order: #309 → #311 → #312. The pro_sources paywall is now wired by the source-monitoring surface (iOS #315 — see the creator-feature stack below). Enforcement stays OFF.
Stacked on the billing foundation, nine more PRs now build the Pro "creator" features the plan contract defines — every one observe-only / capability-gated / dark, nothing enforced: creator export — transcript / script / audio (backend #518 on feat/billing-quota-foundation, iOS #313 on feat/billing-phase4; built + tested); advanced transcript tools — search / tap-to-seek / copy (iOS #314; build succeeded Xcode 26.5, 16 tests); source monitoring — a backend foundation (MonitoredSource model + per-user CRUD + sourceMonitoring gate · backend #520, 62 tests) plus the iOS Sources screen (Profile→Settings) that wires the pro_sources paywall — client gate + server-402→.proSources mapping (iOS #315; build succeeded Xcode 26.5, 13/13 tests); DB-driven plan config — runtime resolves the active plan_versions row from Postgres (30s cache, fail-open to code) + a publish/activate API (backend #519) with a full per-tier plan-config editor UI — edit every value, preview + publish a version, version history / rollback (admin #166; tsc + next build clean); and soundscape per-tier cataloging — a tier column + migration + public-API field + admin set-tier endpoint + seed defaults (backend #521) with the tier-assignment UI in studio/soundscapes (admin #167). Deferred / not built: the source-monitoring polling / auto-generation pipeline (only the foundation shipped), and sound-effects cataloging — proposed only, since there is no sound-effects feature in the app to tier (soundscapes are background-music beds). Enforcement stays OFF.
Status at a glance
Backend PR #514
→ dev · feat/billing-quota-foundation · 17 commits
✅ Contract → ledger → quota → access API → trial
✅ Observe-only metering on all user-initiated paid actions
✅ Enforcement (402) behind per-domain flags + fail-safe
✅ full jest suite 1851/1857 (5 pre-existing on dev, 0 billing)
Backend PR #515
→ dev · feat/billing-grandfather-cohort · 4 commits
✅ Grandfather by filtered cohort (preview + idempotent apply)
✅ Grandfather by hand-picked user IDs (preview + apply)
✅ Campaigns audit list + per-campaign grant drill-down
✅ 29/29 billing+grandfather tests; overlay-safe, bounded, idempotent
Admin PR #163
→ dev (admin.dev) · feat/billing-grants-admin · 11 commits
✅ Grant / revoke / history / operator attribution + access contract
✅ Cohort grandfathering UI (builder → preview → apply)
✅ Users-page multi-select → bulk "Grandfather selected (N)"
✅ Grandfathering audit page + nav; tsc + next build pass
iOS PR #309
→ develop · feat/billing-plan-contract · 9 commits
✅ Decodes expanded /billing/access contract
✅ Fallback plan aligned to Phase-0 numbers
✅ Decodes 402 denial → paywall-placement mapping
✅ base of the iOS stack (#311 + #312 build on it)
iOS PR #311
→ develop · feat/billing-offer-ladder · 6 commits · stacks on #309
✅ Offer-stage rescue ladder: standard → 50% → 65% downsell
✅ Persists highest stage, resumes on reopen, resets on purchase
✅ Commits a stage only after its RC offering loads (P1 fix)
✅ Re-entrancy-guarded dismissal; 13 OfferLadderTests
iOS PR #312
→ develop · feat/billing-phase4 · 12 commits · stacks on #311
✅ Gates: transcript / speed / soundscape / explore-time / file size+count
✅ Trial countdown banner + usage nudges (approaching-limit)
✅ Plan & Billing on live per-meter balances (fallback to legacy)
✅ full iOS stack compiles + 356 tests pass (Xcode 26.5)
Backend PR #518
→ feat/billing-quota-foundation · creator export
✅ Export transcript / script / show-audio (Pro, creatorExport)
✅ Capability-gated, dark by default · built + tested
iOS PR #313
→ feat/billing-phase4 · creator export UI
✅ Transcript / script / audio export surface (Pro)
✅ Built + tested; gated until entitlement + flags live
iOS PR #314
→ feat/billing-phase4 · advanced transcript
✅ Search / tap-to-seek / copy behind advancedTranscript
✅ Build succeeded (Xcode 26.5) · 16 tests
Backend PR #520
→ feat/billing-quota-foundation · source monitoring
✅ MonitoredSource model + per-user CRUD + sourceMonitoring gate
✅ 62 tests · polling / auto-gen deferred
iOS PR #315
→ feat/billing-phase4 · Sources screen (stacks on #312)
✅ Native Sources screen (Profile→Settings) + pro_sources paywall
✅ Client gate + server-402→.proSources map · Xcode 26.5, 13/13 tests
Backend PR #519
→ DB-driven plan config + publish/activate API
✅ Runtime resolves active plan_versions row from Postgres (30s cache)
✅ Fail-open to code contract · built + tested
Admin PR #166
→ feat/billing-grandfathering-admin · plan editor
✅ Edit every plan value per tier · preview + publish a version
✅ Version history / rollback · tsc + next build clean
Backend PR #521
→ dev · soundscape per-tier cataloging
✅ tier column + migration + public-API field + set-tier endpoint
✅ Seed defaults (basic / extended / complete)
Admin PR #167
→ dev · soundscape tier UI
✅ Tier-assignment UI in studio/soundscapes
✅ Built
| Area | State |
|---|---|
| Backend entitlement + quota + enforcement | DONE (dark) |
| iOS billing contract + 402 handling | DONE #309 |
| iOS offer-stage rescue ladder (50%/65% downsell) | DONE #311 |
| iOS Phase-4 gates + trial banner + nudges + live balances | DONE #312 |
| Grandfathering — cohort + hand-picked users + audit | DONE #515 (backend) + #163 (admin UI) |
| Creator export — transcript / script / audio (Pro) | DONE (dark) #518 (backend) + #313 (iOS) |
| Advanced transcript tools — search / tap-to-seek / copy (Pro) | DONE (dark) #314 (iOS) |
Source monitoring (Pro) — model + CRUD + gate + Sources screen + pro_sources | DONE (dark) #520 (backend) + #315 (iOS) · polling deferred |
| DB-driven plan config + plan-config editor UI | DONE (dark) #519 (backend) + #166 (admin) |
| Soundscape per-tier cataloging (tier column + UI) | DONE (dark) #521 (backend) + #167 (admin) |
| Sound-effects cataloging | PROPOSED needs an SFX feature to exist first |
| Product sign-offs (prices, discount duration) | PARTIAL core numbers confirmed |
| App Store + RevenueCat catalog (incl. save50/save65 offerings) | NOT STARTED (yours; checklist ready) |
| Analytics / store QA / % ramp | NOT STARTED (future) |
Review — what I checked & fixed
I did a correctness pass over the whole branch. Findings:
| Check | Result |
|---|---|
| External code-review comments (Greptile P1/P2 + admin security) | ALL FIXED — #514 ba17c42: quota-account update now optimistic-locked on revision with bounded retry (atomicity P1), snapshot limit includes rollover (P2), rollover-approximation documented (P2). #515 79bb1c8: idempotency matches by key alone (P2), filter-hash normalizes implicit defaults (P2), excludeEmailDomains must start with @/. (P1). #309 9b7b8fc5: per-meter balance decode so one bad meter can't drop all balances (P1), tier-aware trial fallback (P2), fileUploads test coverage (P2). #163: grant mutations gated on billing.manage (2× P1 security, confirmed at HEAD). Build + billing tests green after each. |
| New-round review comments (grandfather-by-users + offer ladder + Phase 4) | ALL FIXED · re-reviews green — #515 9164b10: @IsDefined on filters (omitted body → clean 400) + reject past expiresAt (no silently-dead grants). #311 c39b4fb1+065a4584: ladder commits a stage only after its offering loads (peek→load→commit) so a failed load can't strand a user; close button re-entrancy-guarded; and loadPackages(offeringIdentifier:) returns false when busy (never a false "loaded" signal). #312 c32084c4: trial banner refreshes now each minute (no stale "last day" after expiry), nudge-dismiss animates, defensive balance subtracts reserved. #312 rebased onto the fixed ladder; full iOS suite 356/356. Greptile re-reviews on #311 & #312 both COMPLETED / SUCCESS, no open comments. |
| Enforcement could break a request on an internal read error | FIXED a47b39c — the quota peek in enforce() now fails OPEN (logs, never throws); only a genuine over-limit denies. Covers the trial-phase-without-window edge. |
| Concurrent same-window account creation could throw P2002 | FIXED a47b39c — ensureAccount re-reads the row on a race. |
| Metering must never break a user action | OK — all observeUsage/observe calls are fire-and-forget + try/caught; non-fatal by construction. |
| Half-deployed / auth-fail can't downgrade payers | OK — new access fields are additive/optional; iOS treats 404/5xx as "unavailable → keep local", 401/400 never silently grant. |
| Webhook idempotency + out-of-order + transfer + expiry | OK — dedupe by provider event id, stale-event ordering guard, TRANSFER stand-down, read-time expiry; all unit-tested. |
| Enforcement default state | OK — every BILLING_ENFORCE* flag defaults off; verified per-domain + explicit-false override by test. |
| Pre-existing failures (verified) | NOT MINE — the 5 backend failures (posthog-credential, content.getLibrary ×3, generation.duration) fail identically on clean dev; #514 touches none of those files. Zero billing/quota/grandfather failures. |
Known limitations (by design, documented): enforcement uses a read-only peek (small concurrency window vs a strict atomic reserve — the reserve/consume path exists & is tested, ready to wire); enforce() + observeUsage each read the access snapshot, so a generation does two snapshot reads in observe mode (fine on dev; optimizable). Scheduler recurring metering was deferred (see "What's left").
What's done — the billing foundation (59 commits across six PRs)
Backend PR #514 (17 commits)
ba17c42 fix: optimistic-lock reserve + rollover-aware limit (review)
d203b21 recurring=shows/week, one-time=episodes/week (revised numbers)
a47b39c fail-safe hardening (enforce peek fails open; account race re-read)
4e1e491 document BILLING_ENFORCE[_DOMAIN] flags in .env.example
333c46b pre-flight enforcement on generation + document upload
9fe7dc2 enforcement engine behind per-domain flags (402 denials)
4804bf4 observe-only metering for Explore playback + SSE creates
2de1f2e wire generation + document upload into observe-only metering
fda8643 observe-only metering via QuotaService.observe
17fed30 project the trial phase from the RevenueCat webhook
c9caf70 expand GET /billing/access with phase, balances, trial
17b250a access phase + trial fields on billing_access
f546ffc quota service — balances, reserve/finalize/release
8a6e08b UTC-anchored usage-window math for quota meters
f4adc3f usage account + immutable usage ledger schema
8edc016 plan_versions table seeded from the plan contract
2c8591e versioned credit-based plan contract (source of truth)
Backend PR #515 (4 commits)
9164b10 fix: require filters (400 not 500) + reject past expiry (review)
87c7548 grandfather a hand-picked user-id selection + campaign audit
79bb1c8 fix: key-only idempotency, normalized filter hash, @-anchored domain filter (review)
537f62b cohort grandfathering — preview + idempotent bulk apply
iOS PR #309 (9 commits)
9b7b8fc5 fix: per-meter balance decode, tier-aware trial fallback, cover fileUploads (review)
75af88fe fix: per-placement paywall sheet detents
b8253838 test: paywall placement screenshot QA harness
bf61e690 chore: bump build to 126 for the paywall-placement Beta TestFlight
18a8530e sync fallback one-time limits to weekly contract (2/4/10/50)
a5a174e3 react to the backend's structured 402 billing-denial
7dd7e34b tests: expanded /billing/access contract decoding
d8d93c06 align local fallback plan to credit-based Phase-0 contract
772daf4e decode expanded /billing/access contract; extend PlanLimits
iOS PR #311 — offer-stage rescue ladder (6 commits · stacks on #309)
065a4584 fix: don't signal a loaded offering while busy (review)
c39b4fb1 fix: commit ladder stage only after offering load + guard re-entrant dismiss (review)
c32b2a4e test: offer-ladder progression + stage screenshots
2d8b0648 escalate paywall dismissal into the discount downsell
bfbb5112 load a specific RevenueCat offering per ladder stage
ac02f36f offer-stage rescue ladder model + coordinator
iOS PR #312 — Phase 4 (12 commits · stacks on #311)
c32084c4 fix: live trial clock + nudge animation + reserved-aware balance (review)
c204dbd4 test: trial/nudge/balances logic
d920a41e drive Plan & Billing meters from live balances
1efaea0f usage nudges near meter limits
a184aec9 trial countdown banner on home
44b3060d test: entitlement-gate decision logic
d752e333 enforce per-file size + upload-count limits on attach
6d734ef2 enforce explore listening-time via the metered balance
19102972 gate premium soundscapes behind entitlement
e52414b2 enforce playback-speed ceiling on the default-speed slider
ed6d9b13 gate transcript + enforce playback-speed ceiling in the player
67da7bcd entitlement-gate helper + meter-aware explore capability
Admin PR #163 (11 commits)
f9a90fa grandfathering campaigns audit page + nav link
5f92d3f user multi-select + bulk grandfather on the users list
c7273b0 server actions for hand-picked grandfathering + audit
f32591f grandfathering admin page with cohort builder + two-click apply
65b2638 grandfather cohort preview + apply server actions
5bbae77 surface expanded access contract in billing panel (phase/trial/balances)
ad83886 seed grant-form defaults on mount, not during render
75a57e3 correct grant status for not-yet-started grants
6485987 attribute grants to the signed-in operator
82ccee4 billing.manage permission for grant mutations; versioned refreshes
7ae2cd4 admin surface for complimentary grants and access snapshots
New Pro creator-feature stack (nine PRs · stacked on the foundation)
Built this round on top of the billing foundation — all observe-only / capability-gated / dark. Commit-by-commit detail lives on each PR; at a glance:
| PR | What it builds | Base · state |
|---|---|---|
| Backend #518 | Creator export — transcript / script / show-audio behind creatorExport (Pro) | feat/billing-quota-foundation · built + tested |
| iOS #313 | Creator export surface (transcript / script / audio) | feat/billing-phase4 · built + tested |
| iOS #314 | Advanced transcript tools — search / tap-to-seek / copy behind advancedTranscript (Pro) | feat/billing-phase4 · build succeeded (Xcode 26.5), 16 tests |
| Backend #520 | Source-monitoring foundation — MonitoredSource model + per-user CRUD + sourceMonitoring gate (polling / auto-gen deferred) | feat/billing-quota-foundation · 62 tests |
| iOS #315 | Source-monitoring surface — native Sources screen (Profile→Settings) + pro_sources paywall (client gate + server-402→.proSources mapping) | feat/billing-phase4 (stacks on #312) · build succeeded (Xcode 26.5), 13/13 tests |
| Backend #519 | DB-driven plan config — runtime resolves the active plan_versions row from Postgres (30s cache, fail-open to code) + publish/activate API | built + tested |
| Admin #166 | Plan-config editor UI — edit every plan value per tier, preview + publish a version, version history / rollback | feat/billing-grandfathering-admin · tsc + next build clean |
| Backend #521 | Soundscape per-tier cataloging — tier column + migration + public-API field + admin set-tier endpoint + seed defaults | dev · built |
| Admin #167 | Soundscape tier-assignment UI in studio/soundscapes | dev · built |
Not built: the source-monitoring polling / auto-generation pipeline (only the foundation shipped) and sound-effects cataloging — proposed only, since no sound-effects feature exists in the app to tier (soundscapes are background-music beds).
The full pipeline now exists: plan contract → plan_versions (dashboard-editable) → usage ledger → window math → quota service → expanded /billing/access → trial projection → observe-only metering everywhere users act → flag-gated 402 enforcement → iOS decode + placement mapping. Data model added: plan_versions, usage_accounts, usage_ledger, extended billing_access (3 migrations).
Live vs observe-only vs dark
| Capability | State |
|---|---|
| Access snapshot, trial phase, balances, plan versions | live & additive |
| Usage metering (generation, upload, Explore playback, SSE) | observe-only records + logs would-deny, never blocks |
| Enforcement / 402 denials (generation, files, capabilities) | dark wired, flag-gated, off by default |
| iOS 402 decode + placement mapping | dormant activates when flags flip |
| iOS offer ladder / gates / trial banner / nudges / live balances | dormant present but gated on entitlement/trial/balance data; harmless until data + flags are live |
| Creator export · advanced transcript · source-monitoring foundation + Sources screen · soundscape cataloging | dark built, capability-gated, off until entitlement + flags live |
DB-driven plan config (runtime resolves active plan_versions) | live & additive Postgres-backed, 30s cache, fail-open to the code contract |
What's left
Non-code (mostly — the real blockers) Product / Store / Ops · You
- Product sign-offs — prices, 50/65 discount duration, a few rollover caps, cohort/permanent-access policy, nudge caps.
- App Store Connect + RevenueCat catalog — trial intro offer, exact
save50+save65rescue offerings, placements, paywalls, prod webhook, promo/win-back. Missing discount offerings are skipped; standard prices are never presented as a discount. - QA / store test matrix → sandbox → TestFlight → cohort → % ramp.
- Analytics instrumentation + dashboards.
Code (future phases — nice-to-have, not blocking) Backend / iOS / Admin
- iOS — auto-present-a-paywall on a live 402 (typed error + mapping already exposed); a DEBUG
-billingScenario trial/lowBalanceto screenshot the trial banner / nudges / live balances (mock can't synthesize server-only fields today). (The source-monitoring Sources screen +pro_sourcespaywall wiring shipped in #315.) - Backend — offer-state + nudge-decision API (iOS is currently client-authoritative for both), pending plan-change projection, atomic reserve/consume wiring, Explore-URL denial.
- Scheduler recurring metering — deliberately deferred:
GenerationServicehas 41 constructor params + 3 direct-construct specs, too high a blast radius to inject billing safely now. Hook identified (triggerProgramEpisode,schedule.userIdin scope) for a careful standalone change. - Grandfathering hardening — backend (#515: cohort + hand-picked users + audit) + admin UI (#163) are shipped; remaining: two-person approval, an expiry dashboard, a dedicated
billing.grant.bulkpermission. - Source-monitoring pipeline — only the foundation (#520) + the iOS Sources surface (#315) shipped; the polling / change-detection / auto-generation loop is deferred.
- Sound-effects cataloging — proposed only: there is no sound-effects feature in the app to tier yet (soundscapes are background-music beds), so it needs an SFX feature to exist first.
Your action items
| Do | Why / how |
|---|---|
| Review & merge the PRs | Billing foundation (6): backend #514 + #515 → dev; admin #163 → dev; iOS in stack order #309 → #311 → #312 → develop. Creator-feature stack (9): backend #518 + #520 (on feat/billing-quota-foundation) + #519, admin #166 (on feat/billing-grandfathering-admin), iOS #313 + #314 + #315 (on feat/billing-phase4), and #521 + #167 → dev. All additive / observe-only / capability-gated / dark — safe. Merge backend one at a time (no migrate-concurrency guard). |
Create the RevenueCat offerings save50 + save65 | The offer ladder loads these for its 50%/65% downsell. Until they exist the ladder skips that step gracefully (no breakage). Plus the default offering so base paywalls show prices. |
| Upload a build to TestFlight | Via Xcode Organizer → Distribute (needs Xcode 26.5 — default toolchain is 16.4). Test where paywalls + the ladder appear; payments need the RC offerings above. |
| Apply the 3 migrations on dev | Auto on dev-deploy when #514 merges, or start Docker + prisma migrate deploy + npm run seed:plan-versions. |
| Sign off pending product numbers | Unblocks the store catalog. Defaults are versioned config — a data edit, not a redeploy. |
| Run the App Store + RevenueCat setup checklist | The biggest non-code block; gates real purchases. Checklist is turnkey (below). |
| When ready, flip enforcement per domain | BILLING_ENFORCE_CAPABILITIES=true first, verify, then FILES + GENERATION. |
The detailed reports (all in lo/)
PRD source: lo/lissin-monetization-plan/ (docs 01–08). Code — billing foundation: PRs #514 + #515 (backend→dev), #309 + #311 + #312 (iOS→develop, stacked), #163 (admin→dev). Creator-feature stack: #518 + #519 + #520 + #521 (backend), #313 + #314 + #315 (iOS), #166 + #167 (admin). This master report is the single entry point; the reports above go deeper on each dimension.