diff --git a/PRODUCTION_PLAN.md b/PRODUCTION_PLAN.md index e50865f..b4333cd 100644 --- a/PRODUCTION_PLAN.md +++ b/PRODUCTION_PLAN.md @@ -11,36 +11,36 @@ The app (currently "Recurrly") is a working course-built prototype: Clerk auth, ## 1. The Persona Roundtable (collaborative verdicts) **πŸ§‘β€πŸ’» Solopreneur (ships alone, lives on velocity):** -"The prototype's bones are fine β€” don't rewrite, *retrofit*. Persistence, CRUD, notifications, paywall: that's the 80%. Every Phase-2+ feature must have a metric gate or it's scope creep. The course design system is actually a *time gift*: NativeWind's class architecture stays, only tokens and component skins change. Biggest solo risk is reminder reliability β€” it's the one promise that, broken, kills reviews. Budget disproportionate time there. Ship in 8–10 weeks, not 6 months." +"The prototype's bones are fine β€” don't rewrite, _retrofit_. Persistence, CRUD, notifications, paywall: that's the 80%. Every Phase-2+ feature must have a metric gate or it's scope creep. The course design system is actually a _time gift_: NativeWind's class architecture stays, only tokens and component skins change. Biggest solo risk is reminder reliability β€” it's the one promise that, broken, kills reviews. Budget disproportionate time there. Ship in 8–10 weeks, not 6 months." **πŸ’° Billionaire mentor (pattern-matcher across winners):** -"You're not building an app, you're buying distribution with a wedge. Rocket Money proved $500k–$1M/mo exists here (360K ratings, 250–500k downloads/mo, ~$2/download). Their moat is bank-linking; their bleed is trust (1.0/5 customer service, surprise fees, cancellation maze). **The wedge: be the app people switch to angry.** 'Rocket Money alternative' is a free customer-acquisition channel they fund with every bad support interaction. Don't chase $1M MRR β€” chase 10% of their dissatisfied base + the non-US market they structurally can't serve. Own one sentence: *'Your subscriptions, your phone, no bank login.'* Charge less, cancel-anytime in 2 taps, and say so loudly. Money compounds where trust compounds." +"You're not building an app, you're buying distribution with a wedge. Rocket Money proved $500k–$1M/mo exists here (360K ratings, 250–500k downloads/mo, ~$2/download). Their moat is bank-linking; their bleed is trust (1.0/5 customer service, surprise fees, cancellation maze). **The wedge: be the app people switch to angry.** 'Rocket Money alternative' is a free customer-acquisition channel they fund with every bad support interaction. Don't chase $1M MRR β€” chase 10% of their dissatisfied base + the non-US market they structurally can't serve. Own one sentence: _'Your subscriptions, your phone, no bank login.'_ Charge less, cancel-anytime in 2 taps, and say so loudly. Money compounds where trust compounds." **πŸ“£ Marketing head:** -"Category keywords are winnable: 'subscription tracker/manager', 'bill reminder', 'rocket money alternative'. The content engine is the product itself: every cancelled subscription is a shareable stat ('I cut $91/mo'). Build the **share card** (money-saved graphic) into the cancel flow from day one β€” that's the viral loop TikTok's 'subscription audit' format feeds on. Launch sequence: TestFlight beta with 50 Reddit recruits β†’ Product Hunt + Show HN (local-first/E2E angle is HN catnip) β†’ TikTok/Shorts creators in personal-finance niche ($50–200 tests) β†’ ASO iteration. The Midnight Ledger dark aesthetic photographs beautifully in screenshots β€” lean into 'the finance app that doesn't look like a bank form.' Also: the privacy positioning isn't a feature bullet, it's the *headline*. Nobody else in the category can say it." +"Category keywords are winnable: 'subscription tracker/manager', 'bill reminder', 'rocket money alternative'. The content engine is the product itself: every cancelled subscription is a shareable stat ('I cut $91/mo'). Build the **share card** (money-saved graphic) into the cancel flow from day one β€” that's the viral loop TikTok's 'subscription audit' format feeds on. Launch sequence: TestFlight beta with 50 Reddit recruits β†’ Product Hunt + Show HN (local-first/E2E angle is HN catnip) β†’ TikTok/Shorts creators in personal-finance niche ($50–200 tests) β†’ ASO iteration. The Midnight Ledger dark aesthetic photographs beautifully in screenshots β€” lean into 'the finance app that doesn't look like a bank form.' Also: the privacy positioning isn't a feature bullet, it's the _headline_. Nobody else in the category can say it." **πŸ—οΈ Solution architect:** -"Local-first SQLite is the single highest-leverage decision, and the owner's call to run **zero cloud database** takes it further: backups go to the *user's own* iCloud/Google Drive, reports compute on-device, and the only server-side surface is one **stateless Cloudflare Worker** proxying AI calls (key security + rate limiting β€” never ship API keys in a binary). Nothing for us to patch, scale, back up, or breach. The compliance surface shrinks to analytics + auth. One warning: keep every integration behind an interface (`RatesProvider`, `AIProvider`, later `DetectionSource`) β€” vendors (FX APIs, Haiku vs GPT-4o-mini, Plaid vs Tink) will change under you; the Worker makes AI providers swappable server-side without app updates." +"Local-first SQLite is the single highest-leverage decision, and the owner's call to run **zero cloud database** takes it further: backups go to the _user's own_ iCloud/Google Drive, reports compute on-device, and the only server-side surface is one **stateless Cloudflare Worker** proxying AI calls (key security + rate limiting β€” never ship API keys in a binary). Nothing for us to patch, scale, back up, or breach. The compliance surface shrinks to analytics + auth. One warning: keep every integration behind an interface (`RatesProvider`, `AIProvider`, later `DetectionSource`) β€” vendors (FX APIs, Haiku vs GPT-4o-mini, Plaid vs Tink) will change under you; the Worker makes AI providers swappable server-side without app updates." **🎨 UX head:** -"The course template is a liability beyond looks β€” it's *recognizable* to every reviewer who's seen the tutorial. Midnight Ledger fixes distinctiveness, but the deeper UX gaps are flows, not paint: (1) there is no **edit** β€” users' #1 expectation; (2) cancel/delete without undo is data-loss anxiety; (3) empty states are dead ends β€” every empty screen must teach the next action; (4) the aha-moment is seeing the *monthly total shock* β€” onboarding must reach it in <60 seconds via brand-template multi-select, before any signup friction; (5) make auth optional (guest mode) β€” a tracker asking for an account before showing value inverts trust. Gamification must celebrate *not spending* (cancel celebration, savings streaks) β€” that's emotionally unique; every competitor celebrates organizing." +"The course template is a liability beyond looks β€” it's _recognizable_ to every reviewer who's seen the tutorial. Midnight Ledger fixes distinctiveness, but the deeper UX gaps are flows, not paint: (1) there is no **edit** β€” users' #1 expectation; (2) cancel/delete without undo is data-loss anxiety; (3) empty states are dead ends β€” every empty screen must teach the next action; (4) the aha-moment is seeing the _monthly total shock_ β€” onboarding must reach it in <60 seconds via brand-template multi-select, before any signup friction; (5) make auth optional (guest mode) β€” a tracker asking for an account before showing value inverts trust. Gamification must celebrate _not spending_ (cancel celebration, savings streaks) β€” that's emotionally unique; every competitor celebrates organizing." **βš™οΈ CTO:** -"Production risks in the current stack, ranked: NativeWind 5 *preview* in production (pin exact, NW4 fallback documented); no crash reporting (Sentry before beta, non-negotiable); reactCompiler experiment (verify release builds, first flag to pull); iOS 64-notification cap (schedule next-occurrence-only + foreground reconciler); SQLite migrations (never ship one without a fixture test β€” data loss here is unrecoverable brand damage for a local-first app). CI gates from week one: lint, tsc, jest on PR; EAS build on tag. Store compliance is a project, not a checkbox β€” account deletion (Apple 5.1.1(v)), privacy labels, Play data safety, paywall disclosure rules β€” budget 1–2 rejection cycles." +"Production risks in the current stack, ranked: NativeWind 5 _preview_ in production (pin exact, NW4 fallback documented); no crash reporting (Sentry before beta, non-negotiable); reactCompiler experiment (verify release builds, first flag to pull); iOS 64-notification cap (schedule next-occurrence-only + foreground reconciler); SQLite migrations (never ship one without a fixture test β€” data loss here is unrecoverable brand damage for a local-first app). CI gates from week one: lint, tsc, jest on PR; EAS build on tag. Store compliance is a project, not a checkbox β€” account deletion (Apple 5.1.1(v)), privacy labels, Play data safety, paywall disclosure rules β€” budget 1–2 rejection cycles." --- ## 2. What's Already Good (keep it, and why) -| Asset | Why it's genuinely good | Action | -|---|---|---| -| Clerk auth flows (sign-in/up, MFA, secure token cache) | Production-grade auth is weeks of work; done and tested | Keep; make optional (guest mode) | -| PostHog wiring (identify/reset on transitions, event taxonomy started) | Analytics-from-day-one is what most indie apps lack | Keep; extend funnels | -| NativeWind class architecture (`sub-*`, `modal-*`, `picker-*`…) | Component-class abstraction means **reskinning β‰  rebuilding** β€” Midnight Ledger is a token swap + component polish | Keep structure, replace tokens | -| SubscriptionsContext API shape | Screens are already decoupled from storage β€” SQLite slots in behind it | Keep API, swap internals | -| expo-router + typed routes, new-arch, SDK 54 | Modern, correct foundation | Keep | -| Brand-icon resolver + tile system | Already solves the hardest small UX problem (logo consistency) | Keep, extend catalog | -| billing/renewal math in `lib/utils.ts` | Roll-forward logic is correct; just generalize | Extract to `lib/billing.ts` | +| Asset | Why it's genuinely good | Action | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------- | +| Clerk auth flows (sign-in/up, MFA, secure token cache) | Production-grade auth is weeks of work; done and tested | Keep; make optional (guest mode) | +| PostHog wiring (identify/reset on transitions, event taxonomy started) | Analytics-from-day-one is what most indie apps lack | Keep; extend funnels | +| NativeWind class architecture (`sub-*`, `modal-*`, `picker-*`…) | Component-class abstraction means **reskinning β‰  rebuilding** β€” Midnight Ledger is a token swap + component polish | Keep structure, replace tokens | +| SubscriptionsContext API shape | Screens are already decoupled from storage β€” SQLite slots in behind it | Keep API, swap internals | +| expo-router + typed routes, new-arch, SDK 54 | Modern, correct foundation | Keep | +| Brand-icon resolver + tile system | Already solves the hardest small UX problem (logo consistency) | Keep, extend catalog | +| billing/renewal math in `lib/utils.ts` | Roll-forward logic is correct; just generalize | Extract to `lib/billing.ts` | --- @@ -49,8 +49,9 @@ The app (currently "Recurrly") is a working course-built prototype: Clerk auth, **Existing screens to upgrade:** Home (real monthly total from `getMonthlyEquivalent`, not static `HOME_BALANCE`) Β· Subscriptions (search stays) Β· Insights (real aggregations replacing static chart data) Β· Settings (grows: base currency, reminder defaults, appearance, data export, account deletion, manage/cancel Pro) Β· Create modal β†’ **SubscriptionFormModal** (create+edit, brand autocomplete, all 7 cycles, currency picker). **New screens/flows (Phase 0–1):** + 1. **Subscription detail** (`app/subscriptions/[id].tsx` β€” currently dead stub): hero, renewal countdown, price history, actions (Edit/Pause/Cancel/Delete), per-sub reminder settings (Pro). -2. **Onboarding** (`app/onboarding.tsx` β€” dead stub): 2–3 value panes β†’ **brand multi-select grid** (~50 templates w/ regional default prices) β†’ monthly-total reveal (*the aha moment*) β†’ dismissible trial paywall. Guest mode: no signup required until sync/backup. +2. **Onboarding** (`app/onboarding.tsx` β€” dead stub): 2–3 value panes β†’ **brand multi-select grid** (~50 templates w/ regional default prices) β†’ monthly-total reveal (_the aha moment_) β†’ dismissible trial paywall. Guest mode: no signup required until sync/backup. 3. **Paywall** (RevenueCat Paywalls v2, Midnight Ledger skin): trial CTA, restore, EULA/price disclosures. 4. **Cancel-flow celebration + share card**: money-saved graphic generator (the viral loop). 5. **Empty states** for every list (teach next action). @@ -67,6 +68,7 @@ The app (currently "Recurrly") is a working course-built prototype: Clerk auth, **Direction (user-approved):** dark-first premium fintech. Deep ink surfaces, luminous mint money-accent, tabular numerals, quiet glassy elevation. Reference feel: Linear / Copilot Money / Revolut β€” but warmer, calmer. **Tokens (replace `@theme` in `global.css` + `constants/theme.ts`):** + - Background `#0A0E1A` (deep ink) Β· Surface `#131A2E` Β· Surface-raised `#1B2440` Β· Hairline `rgba(255,255,255,0.08)` - Money/positive accent `#4ADE9C` (luminous mint) Β· Alert/renewal-soon `#F4B860` (amber) Β· Destructive `#F47174` Β· Info `#7DA7F4` - Text: primary `#F2F5FA`, secondary `rgba(242,245,250,0.62)`, numerals **tabular-nums** @@ -81,55 +83,57 @@ The app (currently "Recurrly") is a working course-built prototype: Clerk auth, ## 5. Competitive Landscape & Positioning -| Competitor | Price | Excels | Bleeds | -|---|---|---|---| -| **Rocket Money** (4.5β˜…/360K, 250–500k dl/mo, $500k–1M/mo) | Free + $6–12/mo | Bank auto-detection, negotiation, brand | Surprise negotiation fees (35–60% cut), cancellation maze, 1.0/5 support, US-only, satisfaction decays post-month-1 (Trustpilot 3.5 vs store 4.5) | -| **Bobby** (iOS, 4.7β˜…/7.9K) | Free + $0.99–2.99 one-time unlocks | Beloved minimal design, 150+ currencies, service catalog, iCloud sync | **No Android, no iPad** (top complaint), historically unreliable notifications, sync breakage/data loss, 1 reminder/sub, no export, maintenance gaps | -| **TrackMySubs** | Free (10 subs) / **$10/mo** | Business tooling (folders/tags/CSV/Zapier) | Web-first, consumer-expensive, manual-only | -| **Subby** (Android) | Freemium | Android community favorite | No iOS | -| **Apple/Google built-in** | Free | Zero effort | Store-billed subs only β€” misses gym/insurance/direct-billed | +| Competitor | Price | Excels | Bleeds | +| --------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Rocket Money** (4.5β˜…/360K, 250–500k dl/mo, $500k–1M/mo) | Free + $6–12/mo | Bank auto-detection, negotiation, brand | Surprise negotiation fees (35–60% cut), cancellation maze, 1.0/5 support, US-only, satisfaction decays post-month-1 (Trustpilot 3.5 vs store 4.5) | +| **Bobby** (iOS, 4.7β˜…/7.9K) | Free + $0.99–2.99 one-time unlocks | Beloved minimal design, 150+ currencies, service catalog, iCloud sync | **No Android, no iPad** (top complaint), historically unreliable notifications, sync breakage/data loss, 1 reminder/sub, no export, maintenance gaps | +| **TrackMySubs** | Free (10 subs) / **$10/mo** | Business tooling (folders/tags/CSV/Zapier) | Web-first, consumer-expensive, manual-only | +| **Subby** (Android) | Freemium | Android community favorite | No iOS | +| **Apple/Google built-in** | Free | Zero effort | Store-billed subs only β€” misses gym/insurance/direct-billed | **Stance: not head-on vs Rocket Money.** Capture channels: (1) **their churn** β€” every complaint is a switcher; target "Rocket Money alternative" content/ASO from day one; (2) **their non-market** β€” they're US-only; we're global (multi-currency/i18n) into markets where they don't exist; (3) **the bank-averse** β€” a segment they structurally cannot serve. ~10% of their equivalent β‰ˆ $50–100k/mo, bracketing the target. **Complaint-driven differentiators (each maps to a competitor's top negative theme):** reliable multi-reminders (Bobby's failure β€” our core promise) Β· 2-tap cancel of OUR subscription (Rocket Money's rage) Β· free data export always (Bobby's lock-in) Β· transparent single-tier pricing, no surprise fees Β· visibly maintained (changelog, responsive support) Β· cross-platform + iPad day one (Bobby/Subby's structural gap) Β· parity on Bobby PRO features (currencies, categories, app lock, themes, catalog). -**Positioning sentence:** *"The private subscription tracker β€” no bank login, no surprise fees, works offline; your data stays on your phone unless you encrypt-and-sync it."* +**Positioning sentence:** _"The private subscription tracker β€” no bank login, no surprise fees, works offline; your data stays on your phone unless you encrypt-and-sync it."_ --- ## 6. Pricing Strategy -**Philosophy (revised per owner):** the free tier is a *tracker*; Pro is an *AI-personal-finance companion*. Free deliberately excludes every differentiator (AI insights, forecasts, personalized cards, sync, custom reminders) β€” its job is habit formation and funnel volume. The upsell motion is **free β†’ 7-day trial β†’ paid**: the trial exists so users *feel* the AI insights on their own real data, which is what converts. CRO lever: **lead with weekly pricing** (low perceived commitment at the decision moment β€” the Fluently/Blinkist pattern) **anchored against a heavily-discounted annual**; monthly exists but is deliberately unattractive (the decoy). +**Philosophy (revised per owner):** the free tier is a _tracker_; Pro is an _AI-personal-finance companion_. Free deliberately excludes every differentiator (AI insights, forecasts, personalized cards, sync, custom reminders) β€” its job is habit formation and funnel volume. The upsell motion is **free β†’ 7-day trial β†’ paid**: the trial exists so users _feel_ the AI insights on their own real data, which is what converts. CRO lever: **lead with weekly pricing** (low perceived commitment at the decision moment β€” the Fluently/Blinkist pattern) **anchored against a heavily-discounted annual**; monthly exists but is deliberately unattractive (the decoy). -| Tier | Price | Includes | -|---|---|---| -| **Free** | $0 | Up to 5 subs, standard T-1 reminders, current-month totals only, full export, all currencies. **No AI, no forecasts, no history, no sync.** | -| **Pro weekly** | **$1.99/wk** | Everything: AI insights & spend audit, renewal forecasts, personalized savings cards, unlimited subs, custom multi-reminders, full history, encrypted backup/sync (P2), widgets (P2), receipt forwarding (P2), app lock | -| **Pro monthly** (decoy) | $6.99/mo | Same β€” priced to make weekly feel light and annual feel obvious | -| **Pro annual** ⭐ default-selected | **$39.99/yr** (~$0.77/wk, "save 61%") + **7-day free trial** | Same | +| Tier | Price | Includes | +| ---------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Free** | $0 | Up to 5 subs, standard T-1 reminders, current-month totals only, full export, all currencies. **No AI, no forecasts, no history, no sync.** | +| **Pro weekly** | **$1.99/wk** | Everything: AI insights & spend audit, renewal forecasts, personalized savings cards, unlimited subs, custom multi-reminders, full history, encrypted backup/sync (P2), widgets (P2), receipt forwarding (P2), app lock | +| **Pro monthly** (decoy) | $6.99/mo | Same β€” priced to make weekly feel light and annual feel obvious | +| **Pro annual** ⭐ default-selected | **$39.99/yr** (~$0.77/wk, "save 61%") + **7-day free trial** | Same | -*(No Lifetime tier β€” one-time revenue against forever AI inference costs is an unbounded liability; ruled out.)* +_(No Lifetime tier β€” one-time revenue against forever AI inference costs is an unbounded liability; ruled out.)_ **AI cost control (how we don't overspend on tokens):** + 1. **Stateless AI proxy β€” NOT a cloud DB.** A single Cloudflare Worker (free tier: 100k req/day, zero maintenance, no database) holds the API key and enforces per-user rate limits (Clerk JWT verification + Workers KV counter). This is non-negotiable for security: an API key shipped inside the app binary gets extracted and abused within days. Model: **Claude Haiku or GPT-4o-mini class β€” whichever is cheapest per token at build time** (the proxy makes the provider swappable behind one interface). 2. **Generate on data-change, not on view** β€” insights/forecast cards are computed when subscriptions actually change (or max 1Γ—/week on a schedule), cached in the DB, and re-served free on every app open. An insight read costs $0; only regeneration costs tokens. 3. **Per-user monthly token budget** β€” e.g. ~50 AI generations/mo per Pro user enforced at the proxy. When exhausted: degrade gracefully to deterministic (non-LLM) insight templates β€” the math (totals, deltas, forecasts) is computable without AI; the LLM only adds narrative. Users rarely notice a cap this generous. 4. **Cheap model + tight prompts** β€” Haiku-class for insight cards and NL quick-add (~$0.001–0.01 per generation). At 50 generations/mo that's **$0.05–0.50/user/mo against $8.66/mo revenue (annual plan)** β€” sub-6% COGS worst-case. Reserve mid-tier models only for the deep "spend audit" (1–2/user/mo). 5. **Trial abuse guard** β€” trial users get a smaller budget (~15 generations) so free-trial farming can't drain spend. 6. **Spend observability + kill switch** β€” provider budget alerts + a remote-config flag that flips all AI features to template mode instantly if spend anomalies hit. -7. **"Do we buy credits?"** β€” No pre-purchase needed: Anthropic/OpenAI APIs are pay-as-you-go with configurable monthly spend caps at the account level (set it; the proxy degrades gracefully if hit). If a power-user segment emerges later, *sell* consumable "AI boost" credit packs via RevenueCat as a revenue line β€” credits flow toward us, not from us. +7. **"Do we buy credits?"** β€” No pre-purchase needed: Anthropic/OpenAI APIs are pay-as-you-go with configurable monthly spend caps at the account level (set it; the proxy degrades gracefully if hit). If a power-user segment emerges later, _sell_ consumable "AI boost" credit packs via RevenueCat as a revenue line β€” credits flow toward us, not from us. - Paywall presentation: annual pre-selected with trial badge, weekly as the low-commitment escape hatch, monthly as the anchor decoy. All three A/B-tunable remotely via RevenueCat Paywalls v2 without app updates β€” treat these numbers as launch hypotheses, not gospel; the experiment roadmap (price points, trial length, weekly-first vs annual-first layout) is the real strategy. - Unit economics: blended realistic ARPU ~$3.50–5/mo net (weekly converts high but churns fast; annual is the LTV backbone) β€” roughly 2Γ— the prior $2.99 model, and AI inference (Haiku-class, cents/user/mo) is comfortably covered. - **AI moves up the roadmap**: basic AI insights/forecast cards ship in Phase 1.5–2 (pure software, no compliance) since they ARE the paywall. Bank-linked Autopilot remains the Phase-3 Pro+ expansion. -| **Pro+ "Autopilot & AI"** (Phase 2.5–3) | $8.99/mo Β· $59.99/yr | Opt-in bank-linked auto-detection (Plaid US/CA β†’ Tink/TrueLayer EU/UK) **+ the AI Advisor suite** (below); price covers per-account API + inference costs | + | **Pro+ "Autopilot & AI"** (Phase 2.5–3) | $8.99/mo Β· $59.99/yr | Opt-in bank-linked auto-detection (Plaid US/CA β†’ Tink/TrueLayer EU/UK) **+ the AI Advisor suite** (below); price covers per-account API + inference costs | **AI Advisor suite (the scaled-tier value story, and the marketing hook "your personal subscription CFO"):** + - **AI spend audit**: one tap β†’ personalized narrative of waste ("You pay for 3 overlapping streaming services; Claude+ChatGPT+Copilot = $60/mo of AI tools β€” here's what to cut for $312/yr back"). This is the demo-able, screenshot-able wow feature for creators/ads. - **Natural-language quick-add**: "netflix premium 17.99 monthly since march" β†’ parsed subscription (cheap LLM call; falls back to form). - **Price benchmarking**: "You pay $17.99 β€” median for this plan is $15.49; price rose 16% this year" (aggregate anonymized data becomes a moat + PR-able annual "Subscription Inflation Report"). - **Cancellation copilot**: AI-drafted cancellation/negotiation emails per service, paired with the concierge how-to-cancel guides. -- Economics: Haiku-class models keep inference at cents/user/mo β€” margin-safe inside $8.99. Ship AI Advisor *before* bank linking if Plaid integration lags (it's pure software, no compliance) β€” it alone can justify the tier at ~$5.99 interim pricing. +- Economics: Haiku-class models keep inference at cents/user/mo β€” margin-safe inside $8.99. Ship AI Advisor _before_ bank linking if Plaid integration lags (it's pure software, no compliance) β€” it alone can justify the tier at ~$5.99 interim pricing. **Model rationale (RevenueCat 2025/26 benchmarks):** hard-paywall trials convert ~5Γ— freemium (10.7–12.1% vs ~2.1%) and 80–90% of trials start Day 0 β†’ **hybrid**: permanent free tier for top-of-funnel/virality + dismissible trial paywall at end of onboarding + high-intent triggers (6th sub, locked insights). Low price beats high on trial conversion (47.8% vs 28.4%). Undercuts every competitor (~70% under Rocket Money, ~80% under TrackMySubs). Raise later with grandfathering if warranted β€” cutting is harder. @@ -140,9 +144,11 @@ The app (currently "Recurrly") is a working course-built prototype: Clerk auth, ## 7. Technical Roadmap ### Stack risk audit (first) + Pin NativeWind 5-preview exact (NW4 fallback documented) Β· verify release builds with reactCompiler Β· React/RN versions only move with SDK via `expo install --check` Β· secrets to EAS env Β· fix README fiction, broken `reset-project`, replace `pre-commit` pkg with CI. ### Phase 0 β€” Foundation (~2–3 wks) + - **SQLite** (`expo-sqlite`, no ORM): `db/database.ts` (PRAGMA user_version migrations before splash-hide), `db/migrations.ts`, `db/subscriptionsRepo.ts`. Schema v1 sync-ready: UUID ids (`expo-crypto`), status active|paused|cancelled, `billing_cycle`+`custom_interval_days`, materialized `next_renewal_date`, **`is_trial` + `trial_end_date`** (powers trial-expiry alerts), `updated_at`/`deleted_at` tombstones, `kv` table (settings/FX cache/flags). Icons stored as string keys via existing resolver. - **Context keeps its API**, becomes cache over repo; adds update/delete(soft+undo toast)/pause/cancel. Seeds β†’ `__DEV__` only. - **CRUD**: wire `[id].tsx` detail screen + card navigation; modal β†’ dual-mode `SubscriptionFormModal`; wire existing `onCancelPress` props (cancelled rows kept β€” they power money-saved). @@ -151,15 +157,16 @@ Pin NativeWind 5-preview exact (NW4 fallback documented) Β· verify release build - PostHog: `subscription_updated/_cancelled/_deleted/_paused`. ### Phase 1 β€” Launch (~4–6 wks) + - **Rebrand PR**: new name (30–40 candidates β†’ USPTO/EUIPO class 9/42 knockout + domain + store + handles; criteria ≀2 syllables, evokes recurrence/money; seeds: Subwise, Duesday, Outflow, Kept) + **Midnight Ledger token/component swap** + icon/splash. - **Onboarding** (guest-mode first; Clerk account asked only where it earns its place β€” at trial/purchase for entitlement+backup portability): value panes β†’ brand grid β†’ total-shock reveal β†’ trial paywall β†’ optional email opt-in ("monthly spending summary") for guests who skip the trial. Same template data powers form autocomplete. - **Notifications & reminders** (`expo-notifications` β€” all **local/scheduled on-device**, no push server needed, consistent with zero-backend): - - *Renewal reminders (the core promise):* per subscription, next-occurrence-only, **T-3 + T-1 at 9:00 local** (free tier: T-1 fixed; Pro: configurable lead times globally + per-subscription). Notification IDs stored on the row; cancel+reschedule on every edit/pause/cancel/delete; foreground reconciler (AppState) rolls past renewals forward and reschedules β€” this is how we stay under iOS's 64-pending cap and never miss. - - *Free-trial expiry tracking (category killer-feature):* mark any subscription as "trial ends " β†’ aggressive T-2 + T-0 morning alerts β€” "Your Disney+ trial converts to $13.99/mo TOMORROW β€” cancel now?" with a deep link to the cancellation guide. Saves users real money fast β†’ drives the money-saved counter, reviews, and word-of-mouth. - - *Upcoming digest (Pro):* weekly Monday-morning summary β€” "3 renewals this week totaling $47.97." - - *Monthly wrap (Pro):* "Your March: $424.63 across 12 subscriptions, +12% vs Feb" β€” deep-links into the report; doubles as a re-engagement loop. - - *Zombie nudge (Phase 2):* "Still using Canva? You've paid 8 months" β€” capped at 1/month. - - *Hygiene:* Android 13+ contextual permission ask (after first sub added, not at launch) + "Renewal reminders" channel; every notification type individually toggleable in Settings; PostHog events on permission grant/deny + notification-open rates. + - _Renewal reminders (the core promise):_ per subscription, next-occurrence-only, **T-3 + T-1 at 9:00 local** (free tier: T-1 fixed; Pro: configurable lead times globally + per-subscription). Notification IDs stored on the row; cancel+reschedule on every edit/pause/cancel/delete; foreground reconciler (AppState) rolls past renewals forward and reschedules β€” this is how we stay under iOS's 64-pending cap and never miss. + - _Free-trial expiry tracking (category killer-feature):_ mark any subscription as "trial ends " β†’ aggressive T-2 + T-0 morning alerts β€” "Your Disney+ trial converts to $13.99/mo TOMORROW β€” cancel now?" with a deep link to the cancellation guide. Saves users real money fast β†’ drives the money-saved counter, reviews, and word-of-mouth. + - _Upcoming digest (Pro):_ weekly Monday-morning summary β€” "3 renewals this week totaling $47.97." + - _Monthly wrap (Pro):_ "Your March: $424.63 across 12 subscriptions, +12% vs Feb" β€” deep-links into the report; doubles as a re-engagement loop. + - _Zombie nudge (Phase 2):_ "Still using Canva? You've paid 8 months" β€” capped at 1/month. + - _Hygiene:_ Android 13+ contextual permission ask (after first sub added, not at launch) + "Renewal reminders" channel; every notification type individually toggleable in Settings; PostHog events on permission grant/deny + notification-open rates. - **RevenueCat**: `react-native-purchases` + Paywalls v2; `Purchases.logIn(clerkUserId)`; `useEntitlements()` context; gates at 6th sub/insights/settings; 2-tap cancel path in Settings. - **EAS**: eas.json (dev/preview/prod, autoIncrement), bundle IDs, managed credentials, expo-doctor clean. - **Website (submission blocker)**: single-page site on new domain (see Marketing Β§8) hosting the URLs both stores require β€” privacy policy, terms, support, Play account-deletion page β€” plus waitlist. Build early in Phase 1, before store listings are drafted. @@ -167,18 +174,19 @@ Pin NativeWind 5-preview exact (NW4 fallback documented) Β· verify release build - **Quality**: `@sentry/react-native` (sentry-expo deprecated); PostHog screen tracking (`usePathname`) + funnel (installβ†’onboardβ†’first subβ†’3+ subsβ†’paywallβ†’trialβ†’paid); jest-expo tests (billing math, currency, repo CRUD, migration fixtures, screen smokes); GitHub Actions (PR: lint+tsc+jest; tag: EAS build); i18n string extraction NOW (ship EN, cheap while small); TestFlight + Play closed track (12-tester/14-day rule β€” recruit early). ### Phase 2 β€” Growth (mo 3–6, metric-gated) -- **Backup/sync (Pro) β€” NO cloud DB of ours (owner decision: zero server maintenance; local data is the game-changer).** Backups go to the **user's own cloud**: iCloud (iOS) + Google Drive app-data folder (Android) via `react-native-cloud-storage` β€” encrypted SQLite export written to their storage, restored on reinstall/new device. We host nothing, maintain nothing, pay nothing; entitlement-gating still works (the *feature* is Pro, the storage is theirs). **Marketing upgrade: "Your data never touches our servers β€” we don't even have a database."** Trade-offs accepted: no live multi-device sync and no family/household sharing for now β€” if those ever justify a real backend, that's a Phase-3 decision gated on demand + revenue, not a default. + +- **Backup/sync (Pro) β€” NO cloud DB of ours (owner decision: zero server maintenance; local data is the game-changer).** Backups go to the **user's own cloud**: iCloud (iOS) + Google Drive app-data folder (Android) via `react-native-cloud-storage` β€” encrypted SQLite export written to their storage, restored on reinstall/new device. We host nothing, maintain nothing, pay nothing; entitlement-gating still works (the _feature_ is Pro, the storage is theirs). **Marketing upgrade: "Your data never touches our servers β€” we don't even have a database."** Trade-offs accepted: no live multi-device sync and no family/household sharing for now β€” if those ever justify a real backend, that's a Phase-3 decision gated on demand + revenue, not a default. - **Device-change & data recovery (Pro users are the priority; zero cost to us):** - - *Same-platform move (iPhoneβ†’iPhone / Androidβ†’Android) β€” fully automatic:* Pro auto-backup is **ON by default** (every data mutation, debounced, to their iCloud/Drive). On the new device: sign in with Clerk β†’ RevenueCat `logIn` restores the Pro entitlement (works cross-store since it's tied to the user ID, plus the native Restore Purchases button) β†’ app detects the backup in their cloud β†’ one-tap **"Restore your data"** prompt. User does nothing but log in. - - *Cross-platform move (iPhone↔Android) β€” guided manual:* iCloud isn't readable from Android (and vice versa), so the path is the **encrypted export file** (Files/Drive/AirDrop) β†’ login on new device β†’ import. A "Moving to a new phone?" guide in Settings + on the website walks it through step-by-step. - - *Backup-health UX:* Settings shows last-backup timestamp with a stale-backup warning badge (>7 days) for Pro; free users see a gentle "your data lives only on this device β€” export a copy" reminder in Settings and before app deletion where detectable. - - *Disclaimers (set expectations, avoid support fires + 1-star reviews):* onboarding privacy pane + Settings + website FAQ state plainly: *"Your data is stored on your device β€” not on our servers. Pro backs it up automatically to your own iCloud/Google Drive. Without a backup or export, a lost or wiped device means the data cannot be recovered β€” by design."* Same language in the store listing description to preempt refund disputes. - - *Worst case (Pro user, lost device, backup existed):* new device + login β†’ auto-restore β†’ whole event costs us $0 and reads as magic. *Worst case (no backup at all):* data is unrecoverable β€” the disclaimer plus default-on auto-backup makes this rare and clearly communicated. + - _Same-platform move (iPhoneβ†’iPhone / Androidβ†’Android) β€” fully automatic:_ Pro auto-backup is **ON by default** (every data mutation, debounced, to their iCloud/Drive). On the new device: sign in with Clerk β†’ RevenueCat `logIn` restores the Pro entitlement (works cross-store since it's tied to the user ID, plus the native Restore Purchases button) β†’ app detects the backup in their cloud β†’ one-tap **"Restore your data"** prompt. User does nothing but log in. + - _Cross-platform move (iPhone↔Android) β€” guided manual:_ iCloud isn't readable from Android (and vice versa), so the path is the **encrypted export file** (Files/Drive/AirDrop) β†’ login on new device β†’ import. A "Moving to a new phone?" guide in Settings + on the website walks it through step-by-step. + - _Backup-health UX:_ Settings shows last-backup timestamp with a stale-backup warning badge (>7 days) for Pro; free users see a gentle "your data lives only on this device β€” export a copy" reminder in Settings and before app deletion where detectable. + - _Disclaimers (set expectations, avoid support fires + 1-star reviews):_ onboarding privacy pane + Settings + website FAQ state plainly: _"Your data is stored on your device β€” not on our servers. Pro backs it up automatically to your own iCloud/Google Drive. Without a backup or export, a lost or wiped device means the data cannot be recovered β€” by design."_ Same language in the store listing description to preempt refund disputes. + - _Worst case (Pro user, lost device, backup existed):_ new device + login β†’ auto-restore β†’ whole event costs us $0 and reads as magic. _Worst case (no backup at all):_ data is unrecoverable β€” the disclaimer plus default-on auto-backup makes this rare and clearly communicated. - **Dashboard / Reports + insight export (Pro)** β€” evolves the Insights tab into a full reporting surface (all computed **on-device**, zero backend): monthly/annual spend reports, category & currency breakdowns, cycle mix, price-change history, money-saved ledger, year-over-year trends, AI narrative summary on top (via the Worker proxy). **Export**: shareable PDF report via `expo-print` (AI summary + charts + tables β€” "my subscription annual report"), plus CSV of any view; the share-card system reuses the same rendering. Pro-gated; the free tier sees current-month totals only β€” this is a core paywall feature and a B2B/freelancer hook (expense reports for taxes). -- **Email-forward parsing (Pro)** β€” *deferred behind its own gate (~$5k MRR) since it requires the first real backend surface* (inbound mail webhook + short-lived storage, e.g. Postmark + a single worker; raw deleted ≀72h, parsed suggestions delivered to the app via push payload, never auto-added). No OAuth/CASA needed. Decision on whether to build it at all happens then β€” the AI Advisor + reports may carry the tier without it. -- **Gamification (kept honest β€” celebrate *not spending*, never dark-pattern):** money-saved counter + cancel celebration/share card, savings streaks, zombie nudges (`last_reviewed_at`), price-increase alerts (`price_history` migration), **savings milestones** (badges at $100/$500/$1k cancelled β€” each milestone triggers the share card AND the rating funnel below), monthly "audit ritual" (a 2-minute guided review of all subs, streak-counted β€” habit loop that drives retention, our D30 lever). +- **Email-forward parsing (Pro)** β€” _deferred behind its own gate (~$5k MRR) since it requires the first real backend surface_ (inbound mail webhook + short-lived storage, e.g. Postmark + a single worker; raw deleted ≀72h, parsed suggestions delivered to the app via push payload, never auto-added). No OAuth/CASA needed. Decision on whether to build it at all happens then β€” the AI Advisor + reports may carry the tier without it. +- **Gamification (kept honest β€” celebrate _not spending_, never dark-pattern):** money-saved counter + cancel celebration/share card, savings streaks, zombie nudges (`last_reviewed_at`), price-increase alerts (`price_history` migration), **savings milestones** (badges at $100/$500/$1k cancelled β€” each milestone triggers the share card AND the rating funnel below), monthly "audit ritual" (a 2-minute guided review of all subs, streak-counted β€” habit loop that drives retention, our D30 lever). - **Trust & Community Loop (loyalty engine):** - - **Sentiment-gated rating funnel**: at peak-happiness moments (milestone hit, money-saved celebration, 3rd week streak) ask "Enjoying it?" β†’ happy β†’ native store review prompt (`expo-store-review` β€” iOS SKStoreReviewController / Play In-App Review). Unhappy β†’ private feedback form routed to us, not the store. *Compliance note: stores prohibit prefilled/incentivized review text and there is no API to post reviews β€” the native prompt is the only legal path, and this funnel is the proven way to bend store ratings upward.* + - **Sentiment-gated rating funnel**: at peak-happiness moments (milestone hit, money-saved celebration, 3rd week streak) ask "Enjoying it?" β†’ happy β†’ native store review prompt (`expo-store-review` β€” iOS SKStoreReviewController / Play In-App Review). Unhappy β†’ private feedback form routed to us, not the store. _Compliance note: stores prohibit prefilled/incentivized review text and there is no API to post reviews β€” the native prompt is the only legal path, and this funnel is the proven way to bend store ratings upward._ - **AI-drafted testimonials (website, not stores)**: after a happy signal, offer 3 AI-generated quote drafts based on the user's real stats ("Cancelled $34/mo of zombie subs in my first week") β†’ user picks/edits β†’ one-tap consent β†’ published to the website testimonial wall + optional social share via the share-card system. Gets the "predefined description" convenience where it's actually allowed. - **In-app feature request board**: embed **Canny free tier** (or Featurebase) in a webview β€” no database of ours to maintain. Submit + upvote + status labels (Planned/Building/Shipped). **"You asked β†’ we shipped"** section in every changelog release note β€” the personal-connect loop. Founder replies to top requests in-app. - **In-app changelog / What's New** screen fed from a simple JSON β€” visible active maintenance is itself a differentiator vs Bobby's abandonment reputation. @@ -186,16 +194,19 @@ Pin NativeWind 5-preview exact (NW4 fallback documented) Β· verify release build - **Platform**: widgets (`@bacons/apple-targets` + Glance; JSON bridge β€” budget a week+), app lock (`expo-local-authentication`), light-theme variant, locales DE/FR/ES/PT-BR/JA, referral via RC offer codes. ### Phase 3 β€” Revenue engine (mo 6–18) + - **Autopilot tier** ($8.99/$59.99): Plaid recurring-transactions endpoint (US/CA β†’ Tink/TrueLayer EU/UK), server-side by necessity, opt-in ("local by default, linked only if you choose"). Gate: ~$3–5k MRR. - **Gmail OAuth** (restricted scope + CASA Tier 2) β€” gate >$5k MRR; reuses parsing pipeline. Cheaper global detection than bank APIs. - **No %-fee negotiation** (Rocket Money's #1 complaint) β†’ cancellation-concierge guides instead; flat-fee partnerships much later. - **Family sharing (two levels):** - - *Level 1 β€” entitlement sharing (near-free, ship early in Phase 2):* enable **Apple Family Sharing / Google Play family library on the Pro IAP** β€” RevenueCat supports family-shared subscriptions natively. One household buys Pro once, everyone's app unlocks. Zero backend, strong paywall selling point ("Pro for your whole family"). - - *Level 2 β€” shared household subscription list (Phase 3, gated on demand + revenue):* actually seeing/editing one shared list requires either a real backend or CloudKit shared databases (Apple-only). Deferred until the demand signal justifies breaking the zero-backend rule; the feature-request board tells us when. + - _Level 1 β€” entitlement sharing (near-free, ship early in Phase 2):_ enable **Apple Family Sharing / Google Play family library on the Pro IAP** β€” RevenueCat supports family-shared subscriptions natively. One household buys Pro once, everyone's app unlocks. Zero backend, strong paywall selling point ("Pro for your whole family"). + - _Level 2 β€” shared household subscription list (Phase 3, gated on demand + revenue):_ actually seeing/editing one shared list requires either a real backend or CloudKit shared databases (Apple-only). Deferred until the demand signal justifies breaking the zero-backend rule; the feature-request board tells us when. - B2B wedge test via Business category + CSV export. ### Future roadmap add-ons (high value, low effort β€” no architectural rework) + All of these ride on the existing SQLite + design system + report/share infrastructure: + - **Calendar view** of upcoming renewals (month grid β€” data already materialized in `next_renewal_date`) + **"add renewals to device calendar"** via `expo-calendar` (Pro). - **CSV import** (not just export) β€” one-tap switching path for TrackMySubs/spreadsheet users; a competitor-conversion feature that costs a parser. - **"Year in Subscriptions" Wrapped** (December seasonal): shareable animated recap built entirely from the existing report + share-card systems β€” annual viral moment. @@ -211,13 +222,13 @@ All of these ride on the existing SQLite + design system + report/share infrastr ## 8. Marketing & Launch Plan - **Single-page marketing website (Phase-1 deliverable β€” also a hard store-submission dependency):** - - *Why it blocks launch:* App Store requires a **support URL** + **privacy policy URL**; Play additionally requires a **web account-deletion URL** and Data-Safety-consistent privacy policy. No site β†’ no approval. - - *Structure (Fluently-style single page, Midnight Ledger branding):* hero (one-liner + phone mockup + store badges / **waitlist form pre-launch**) β†’ the $91/mo pain stat β†’ 3 feature blocks (reliable reminders Β· privacy/no-bank-login Β· money-saved gamification) β†’ live testimonial wall (fed by the in-app consented-quote flow) β†’ pricing β†’ FAQ β†’ footer (privacy, terms, support email, account-deletion page, changelog). - - *Build:* static Astro or Next.js on Vercel on the new domain (~2–3 days); waitlist via a form service (Loops / Tally / Formspree β€” no database of ours; doubles as the launch-announcement list); OG images from the share-card generator for link previews. - - *Post-launch growth:* same site hosts the "how to cancel X" SEO guide pages and the annual Subscription Inflation Report β€” the site graduates from brochure to acquisition channel. + - _Why it blocks launch:_ App Store requires a **support URL** + **privacy policy URL**; Play additionally requires a **web account-deletion URL** and Data-Safety-consistent privacy policy. No site β†’ no approval. + - _Structure (Fluently-style single page, Midnight Ledger branding):_ hero (one-liner + phone mockup + store badges / **waitlist form pre-launch**) β†’ the $91/mo pain stat β†’ 3 feature blocks (reliable reminders Β· privacy/no-bank-login Β· money-saved gamification) β†’ live testimonial wall (fed by the in-app consented-quote flow) β†’ pricing β†’ FAQ β†’ footer (privacy, terms, support email, account-deletion page, changelog). + - _Build:_ static Astro or Next.js on Vercel on the new domain (~2–3 days); waitlist via a form service (Loops / Tally / Formspree β€” no database of ours; doubles as the launch-announcement list); OG images from the share-card generator for link previews. + - _Post-launch growth:_ same site hosts the "how to cancel X" SEO guide pages and the annual Subscription Inflation Report β€” the site graduates from brochure to acquisition channel. - **Lead capture & owned audience (works WITH guest mode):** RevenueCat can't provide emails β€” Apple/Google never share purchaser contact info β€” so leads are captured in-app at value moments, all flowing into one marketing list (Loops or MailerLite; PostHog-linked): - 1. **Paying customers (the priority leads): login is a HARD requirement before checkout.** The trial/purchase flow enforces Clerk sign-up first β€” honest pitch: *"Create your account so Pro and your backups follow you to any device"* (genuinely required for the device-change story above). Note: actual payment details (card/billing) go to Apple/Google's native purchase sheet and are never visible to us or RevenueCat β€” the **Clerk profile is the lead record**, linked to the RevenueCat entitlement by user ID. Result: ~100% of paid users are known, emailable leads. - 2. **Guest/free users β€” capped contextual signup prompts + value-exchange:** periodic sign-up/login prompts are fine but frequency-capped and dismissible (e.g., at natural moments β€” after 3rd subscription added, after a milestone, at most once per week β€” never blocking core use). Rationale: constant nag popups are precisely what Rocket Money gets punished for in reviews; capped prompts capture leads without spending trust. Alongside: the optional *"Email me my monthly spending summary"* / price-increase-alert opt-ins (real benefits, not gates). Expect 15–30% combined opt-in vs ~0% from a hard gate they'd abandon. + 1. **Paying customers (the priority leads): login is a HARD requirement before checkout.** The trial/purchase flow enforces Clerk sign-up first β€” honest pitch: _"Create your account so Pro and your backups follow you to any device"_ (genuinely required for the device-change story above). Note: actual payment details (card/billing) go to Apple/Google's native purchase sheet and are never visible to us or RevenueCat β€” the **Clerk profile is the lead record**, linked to the RevenueCat entitlement by user ID. Result: ~100% of paid users are known, emailable leads. + 2. **Guest/free users β€” capped contextual signup prompts + value-exchange:** periodic sign-up/login prompts are fine but frequency-capped and dismissible (e.g., at natural moments β€” after 3rd subscription added, after a milestone, at most once per week β€” never blocking core use). Rationale: constant nag popups are precisely what Rocket Money gets punished for in reviews; capped prompts capture leads without spending trust. Alongside: the optional _"Email me my monthly spending summary"_ / price-increase-alert opt-ins (real benefits, not gates). Expect 15–30% combined opt-in vs ~0% from a hard gate they'd abandon. 3. **Website waitlist** (pre-launch) + newsletter (the annual Subscription Inflation Report is the flagship email). 4. Compliance: explicit marketing-consent checkbox (GDPR), disclosed in privacy policy/labels; unsubscribe honored via the email provider. - This list is the cross-promotion asset for launching future apps β€” owned, portable, and independent of store algorithms. @@ -231,20 +242,20 @@ All of these ride on the existing SQLite + design system + report/share infrastr ## 9. Risks & Challenges (foreseen upfront β†’ tackled) -| Risk | Tackle | -|---|---| -| Reminder unreliability (kills the core promise) | Next-occurrence-only scheduling + foreground reconciler + dev inspector screen + aggressive device-matrix testing; it's the top QA budget line | -| NativeWind 5 preview breaks a release | Exact pins, lockfile snapshot, documented NW4 fallback, preview-build smoke checklist | -| Course-clone recognizability | Full Midnight Ledger reskin + rename in one Phase-1 PR (structure preserved, cheap) | -| Store rejections (deletion, paywall rules, labels) | Compliance checklist executed pre-submission; 1–2 rejection cycles budgeted into timeline | -| SQLite migration data loss (unrecoverable for local-first) | Every migration ships with a fixture-DB jest test, no exceptions | -| Trademark collision post-launch | Knockout search before buying assets; brand strings centralized (forced rename = 1 day) | -| Solo scope creep | Every Phase-2+ item metric-gated (sync @500 MAU, Autopilot @$3–5k, Gmail @$5k) | -| Low willingness-to-pay anchored by Bobby's $2.99 one-time | Paywall sells the AI companion + ongoing-cost features (sync, parsing, reliability), never manual entry β€” different category, different anchor. No lifetime SKU (AI inference makes one-time pricing an unbounded liability) | -| AI token overspend / abuse | Server-side proxy w/ per-user budgets, generate-on-change caching, Haiku-class models (<6% COGS), trial caps, provider spend limits + template-mode kill switch (see Β§6) | -| Clerk-required auth = onboarding friction + compliance surface | Guest mode in Phase 1; account only for Pro sync | -| FX API disappearance | Bundled fallback rates + `RatesProvider` abstraction; totals degrade to "rates as of " | -| Play new-account 12-tester/14-day rule | Recruit testers during Phase 0 | +| Risk | Tackle | +| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Reminder unreliability (kills the core promise) | Next-occurrence-only scheduling + foreground reconciler + dev inspector screen + aggressive device-matrix testing; it's the top QA budget line | +| NativeWind 5 preview breaks a release | Exact pins, lockfile snapshot, documented NW4 fallback, preview-build smoke checklist | +| Course-clone recognizability | Full Midnight Ledger reskin + rename in one Phase-1 PR (structure preserved, cheap) | +| Store rejections (deletion, paywall rules, labels) | Compliance checklist executed pre-submission; 1–2 rejection cycles budgeted into timeline | +| SQLite migration data loss (unrecoverable for local-first) | Every migration ships with a fixture-DB jest test, no exceptions | +| Trademark collision post-launch | Knockout search before buying assets; brand strings centralized (forced rename = 1 day) | +| Solo scope creep | Every Phase-2+ item metric-gated (sync @500 MAU, Autopilot @$3–5k, Gmail @$5k) | +| Low willingness-to-pay anchored by Bobby's $2.99 one-time | Paywall sells the AI companion + ongoing-cost features (sync, parsing, reliability), never manual entry β€” different category, different anchor. No lifetime SKU (AI inference makes one-time pricing an unbounded liability) | +| AI token overspend / abuse | Server-side proxy w/ per-user budgets, generate-on-change caching, Haiku-class models (<6% COGS), trial caps, provider spend limits + template-mode kill switch (see Β§6) | +| Clerk-required auth = onboarding friction + compliance surface | Guest mode in Phase 1; account only for Pro sync | +| FX API disappearance | Bundled fallback rates + `RatesProvider` abstraction; totals degrade to "rates as of " | +| Play new-account 12-tester/14-day rule | Recruit testers during Phase 0 | ## 10. Verification & Go-Live Checklist diff --git a/__tests__/analytics.test.ts b/__tests__/analytics.test.ts new file mode 100644 index 0000000..05a3a28 --- /dev/null +++ b/__tests__/analytics.test.ts @@ -0,0 +1,18 @@ +import { priceBucket } from "@/lib/analytics"; + +describe("priceBucket", () => { + it("bands monthly amounts without exposing exact values", () => { + expect(priceBucket(2.99)).toBe("under_5"); + expect(priceBucket(9.99)).toBe("5_15"); + expect(priceBucket(15)).toBe("15_50"); + expect(priceBucket(49.99)).toBe("15_50"); + expect(priceBucket(77.49)).toBe("50_plus"); + }); + + it("handles boundaries and invalid input", () => { + expect(priceBucket(5)).toBe("5_15"); + expect(priceBucket(50)).toBe("50_plus"); + expect(priceBucket(0)).toBe("unknown"); + expect(priceBucket(NaN)).toBe("unknown"); + }); +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 9a1e5ee..cd5ab31 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -8,6 +8,7 @@ import images from "@/constants/images"; import { useCurrency } from "@/context/CurrencyContext"; import { useSubscriptions } from "@/context/SubscriptionsContext"; import "@/global.css"; +import { priceBucket } from "@/lib/analytics"; import { getDaysUntilRenewal, getMonthlyEquivalent } from "@/lib/billing"; import { formatCurrency } from "@/lib/utils"; import { useClerk, useUser } from "@clerk/expo"; @@ -84,14 +85,19 @@ export default function App() { const handleCreate = (draft: SubscriptionDraft) => { const created = addSubscription(draft); + // Non-identifying signal only: no name, no exact price, no currency. posthog.capture("subscription_created", { subscription_id: created.id, - name: created.name, - price: created.price, - currency: created.currency ?? "USD", billing_cycle: created.billingCycle ?? "monthly", category: created.category ?? "Uncategorized", is_trial: !!created.isTrial, + price_bucket: priceBucket( + getMonthlyEquivalent( + created.price, + created.billingCycle ?? "monthly", + created.customIntervalDays, + ), + ), }); }; diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 1511fd6..ec0b6d7 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -2,11 +2,14 @@ import PickerSheet, { type PickerItem } from "@/components/PickerSheet"; import { CURRENCY_CODES, currencyName } from "@/constants/currencies"; import { useCurrency } from "@/context/CurrencyContext"; import images from "@/constants/images"; +import { getKv, setKv } from "@/db/subscriptionsRepo"; +import { ANALYTICS_OPTOUT_KEY } from "@/lib/analytics"; import { useClerk, useUser } from "@clerk/expo"; import dayjs from "dayjs"; import { styled } from "nativewind"; +import { usePostHog } from "posthog-react-native"; import { useState } from "react"; -import { Image, Pressable, ScrollView, Text, View } from "react-native"; +import { Image, Pressable, ScrollView, Switch, Text, View } from "react-native"; import { SafeAreaView as RNSafeAreaView } from "react-native-safe-area-context"; const CURRENCY_ITEMS: PickerItem[] = CURRENCY_CODES.map((code) => ({ @@ -20,7 +23,21 @@ const Settings = () => { const { signOut } = useClerk(); const { user } = useUser(); const { baseCurrency, setBaseCurrency } = useCurrency(); + const posthog = usePostHog(); const [showCurrencyPicker, setShowCurrencyPicker] = useState(false); + const [analyticsEnabled, setAnalyticsEnabled] = useState( + () => getKv(ANALYTICS_OPTOUT_KEY) !== "1", + ); + + const toggleAnalytics = (enabled: boolean) => { + setAnalyticsEnabled(enabled); + setKv(ANALYTICS_OPTOUT_KEY, enabled ? "0" : "1"); + if (enabled) { + posthog.optIn(); + } else { + posthog.optOut(); + } + }; const displayName = user?.firstName || user?.fullName || user?.emailAddresses[0]?.emailAddress?.split("@")[0] || "User"; const email = user?.emailAddresses[0]?.emailAddress || "No email"; @@ -80,6 +97,18 @@ const Settings = () => { {baseCurrency} Β· {currencyName(baseCurrency)} β–Ύ + + + + + Share anonymous analytics + + + Usage only β€” never your subscription names, amounts, or email. + + + + diff --git a/app/_layout.tsx b/app/_layout.tsx index 672167b..fef1e57 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -9,12 +9,23 @@ import { PostHogProvider, usePostHog } from "posthog-react-native"; import { CurrencyProvider } from "@/context/CurrencyContext"; import { SubscriptionsProvider } from "@/context/SubscriptionsContext"; +import { getKv } from "@/db/subscriptionsRepo"; +import { ANALYTICS_OPTOUT_KEY } from "@/lib/analytics"; SplashScreen.preventAutoHideAsync(); function PostHogUserIdentifier() { const posthog = usePostHog(); const { isLoaded, isSignedIn, user } = useUser(); + + // Apply the persisted analytics opt-out on launch (before any capture). + useEffect(() => { + if (getKv(ANALYTICS_OPTOUT_KEY) === "1") { + posthog.optOut(); + } else { + posthog.optIn(); + } + }, [posthog]); // Tracks the previous auth state so we only capture on real transitions. // `null` means "not yet known" (initial mount), which must not count as a // sign-in/sign-out event (e.g. reopening the app while already signed in). @@ -26,15 +37,10 @@ function PostHogUserIdentifier() { } if (isSignedIn && user) { - const properties: Record = {}; - const email = user.primaryEmailAddress?.emailAddress; - if (email) { - properties.email = email; - } - if (user.fullName) { - properties.name = user.fullName; - } - posthog.identify(user.id, properties); + // Identify by the opaque Clerk id only. PII (email/name) intentionally + // stays out of analytics β€” it lives in Clerk (auth) and, later, the + // marketing list. PostHog holds behavior keyed to this id. + posthog.identify(user.id); // Fire only on a genuine signed-out -> signed-in transition. if (wasSignedIn.current === false) { diff --git a/app/subscriptions/[id].tsx b/app/subscriptions/[id].tsx index 84197f1..ecf0ee2 100644 --- a/app/subscriptions/[id].tsx +++ b/app/subscriptions/[id].tsx @@ -102,11 +102,9 @@ const SubscriptionDetail = () => { const isPaused = subscription.status === "paused"; const isCancelled = subscription.status === "cancelled"; + // Opaque id only β€” no subscription name in analytics. const captureStatus = (event: string) => - posthog.capture(event, { - subscription_id: subscription.id, - name: subscription.name, - }); + posthog.capture(event, { subscription_id: subscription.id }); const handleEdit = (draft: SubscriptionDraft) => { updateSubscription(subscription.id, draft); diff --git a/lib/analytics.ts b/lib/analytics.ts new file mode 100644 index 0000000..aad30ac --- /dev/null +++ b/lib/analytics.ts @@ -0,0 +1,17 @@ +/** + * Coarse, non-identifying spend bucket for analytics. We never send exact + * amounts to analytics β€” only which band a subscription falls into β€” so we + * keep pricing insight without exposing a user's actual financials. + * + * @param monthly - monthly-equivalent amount in the user's base currency. + */ +export const priceBucket = (monthly: number): string => { + if (!Number.isFinite(monthly) || monthly <= 0) return "unknown"; + if (monthly < 5) return "under_5"; + if (monthly < 15) return "5_15"; + if (monthly < 50) return "15_50"; + return "50_plus"; +}; + +/** kv flag key for the analytics opt-out preference. */ +export const ANALYTICS_OPTOUT_KEY = "analytics_optout";