Insights — internal
Enter the password to continue.
Thousand User Readiness

Six parallel audits · 15 August 2026

Thousand User Readiness

What happens to Insights if a thousand people arrive next week behind serious ad spend. Six agents swept the app in parallel — onboarding, deck, recommender, monetization, scale and analytics. Everything below was verified against live code and the production database.

A correction, first

"People behave like readers, not swipers" was wrongMy error

I reported that conclusion confidently and put it in the wiki as a redesign input. It was an artifact of comparing two different time periods.

this_is_me and pass are emitted only by the swipe deck, and their first rows ever are dated 10 August 2026 — the day the deck shipped. The story_view (2,477) and opened (652) counts I set them against reach back to April: four months of the old scroll feed, a surface with no keep/pass concept at all.

Scoped to the deck's actual five-day lifetime, six distinct humans have used it — and among them the swipe is the dominant action:

Action since 10 AugEventsPeople
pass (swipe left)294
save_for_later (swipe right)294
this_is_me (swipe right)133
opened (tap to read)226

71 swipe actions against 22 opens — and the opens are themselves roughly 40% inflated by a double-write. The swipe is being used. There is no reader-versus-swiper problem to design around; there is a five-day-old surface with almost no traffic on it yet.

Why 45% of your users never activated

The live build can permanently lock a user out — twice overExplains everything

This is not a design problem. It is two bugs, both live today, both already fixed on the work branch.

Lockout one — bounce off the onboarding paywall and you can never get back in. The onboarding paywall is presented with showNoThansks: false and no dismiss (OnboardingFlowView.swift:1435). The escape hatch that records you reached it — onboardingPaywallReached — has zero occurrences on paths-v1 and six on the work branch. So a user who quits at that screen replays all nine onboarding screens on next launch and meets the identical wall. They can never reach the free experience. The only exit is subscribing.

Lockout two — deep-link arrivals loop forever. On live, completing the onboarding sheet from a deep link never sets hasSeenOnboarding. Tap a card → onboard → hit the hard paywall → complete it → tap another card → onboard again. Infinite, and it poisons the entire share and SEO acquisition funnel.

239 users, 130 of whom ever generated a single interaction. That 109-person gap is the wall. And it is real behaviour, not an instrumentation artifact — I computed it from card_interactions rows in the database, not from Amplitude.

Driving a thousand paid installs into this build means paying for a lockout.

The three things that would waste the spend

In the order they'd cost you money.

1 · Meta cannot see conversions from the live buildBlocks attribution

Three independent failures on paths-v1, all fixed on the work branch:

  • No FacebookClientToken — the SDK initialises but can't authenticate, so its event stream doesn't land.
  • No ATT promptrequestTrackingAuthorization doesn't exist on live. Users are never asked, so the IDFA is never available.
  • No ATT gatingisAdvertiserTrackingEnabled is never set, so FBSDK won't use the IDFA even if granted.

SKAdNetwork still works (7 call sites, both SKAN ids are Meta's) so you'd get coarse delayed postbacks — and nothing else.

Compounding it: Meta only ever receives .startTrial and .subscribe. There is no signup event. Meta needs ~50 conversions per ad set per week to leave the learning phase; you have 11 payers in the product's lifetime. A purchase-optimised campaign cannot converge. Wire CompleteRegistration on signup in the same build.

2 · The gift flow can hand out subscriptions to anyoneDirect revenue risk

500 unclaimed codes, created 9 August, all exactly 18 characters — Apple's subscription Offer Code format. IAP promo codes are 12 characters and capped at 100 per version, so 500 of them cannot be cards10 codes.

RootView.swift:62-69 presents ReferFriendSheet() on nothing but pendingReferral && state == .contentno entitlement check, and it's live. Anyone who opens insights://refer or the /refer page can draw a code. The app then grants no cards, because only cards10 is credited — so the user is also told something untrue.

Check App Store Connect before shipping, and gate or disable that entry.

3 · Customer.io cannot tell a payer from a free userBlocks lifecycle

Six of the thirteen identify traits exist only on the work branch: notification_consent, plan_name, is_subscriber, subscription_expires_at, ideas_read, my_insights_ready.

Five of the nine planned CRM segments are therefore unbuildable — Trialist, Paid/Power, Cancelled and both dormancy tiers all need plan state that never arrives. Every trial→paid and win-back campaign has no attribute to run on.

Push reachability is 15.9% (38 of 239). 77 users granted OS permission in 90 days; only 38 tokens exist. The live build registers for APNs exactly once, inside onboarding, and the consent write races the async callback so the row is often stored with a null token.

The thing that has been silently broken for eleven days

The overnight recommender has never produced a single cardConfirmed

The prompt asks Claude for exactly 3 cards (recommendation_agent.py:320, and the JSON schema at :328 has three slots). The gate immediately after demands ten:

DAILY_CARD_COUNT = 10line 33
picks = [...][:DAILY_CARD_COUNT]line 339
if len(picks) < DAILY_CARD_COUNT: return Noneline 340 — always true

The Haiku call runs and is billed at line 330, then the result is discarded at line 340. Every user, every night.

Verified in the data: of 191 prepared_cards rows, zero were written in the 03:00 window and zero carry journey_stage='live' — the stage only the agent emits. Every row comes from the request path or onboarding. Origin: commit b9cd3e5 on 4 August bumped the constant 3→10 and never touched the prompt.

At 1,000 users that is roughly $99/month of Anthropic spend producing nothing. The fix is one line.

What breaks, in order, as users arrive

Measured against live infrastructure: Supabase Micro — 2 burstable cores, max_connections 60 — and a single uvicorn worker.

239 today 1,239 ~45 readers /v2/signal saturates DB 781 signups /v1/paths silently truncates 1,000 profiles nightly job stops covering users All three fail silently — no error, no log, no alert
Every one of these returns HTTP 200. The symptom you'd see is latency, then wrong content — never an exception.
1
Every slide swipe triggers a full recommender recompute. routers/cards.py:905 schedules recompute_and_cache on every signal, ungated by type. story_view fires per slide and is 57% of all traffic. Each recompute is 19–26 serial round-trips plus 2–3 match_cards calls at a measured 222 ms brute-force seq scan each — about 0.55 s of database CPU per swipe. Two cores gives a ceiling near 45 concurrently-reading users. One condition at line 900 removes ~90% of it.
2
/v1/paths truncates at 781 signups — mid-week. routers/paths.py:96 reads user_path_progress unpaged; PostgREST caps at 1,000 rows and returns 200. At 1.285 rows per signup you cross it partway through the campaign. It feeds the onboarding journey picker's ranking, then freezes into a 30-minute cache. fetch_all() already exists.
3
Card reads do 30 sequential seq scans. stories, quests and key_insights have no index on card_id, so every /v2/daily filters ~50,000 rows and touches ~24 MB. Three indexes take 55 ms to about 2 ms.
4
The nightly job stops covering users at exactly 1,000 profiles. Both recommendation_agent.py:369 and recommender.py:870 enumerate users unpaged. Everyone past the first 1,000 falls through to the same generic popular-cards deck, forever, with no log.
5
The self-model is already truncating today. self_model.py:130 is unpaged and one user already has 1,166 interactions — their map, clusters and mirror sentence are computed from an arbitrary 1,000-row slice right now. At 1,000 users this becomes routine for every engaged reader.
6
/v1/favorite is a textbook N+1. One card at a time, each spinning a fresh thread pool. Forty favourites ≈ 80 round-trips and ~7 seconds. The batch function already exists.

What it costs

LineTodayAt ~1,240 usersNote
Anthropic — nightly agent~$18~$92100% wasted until the one-line fix
Anthropic — go-deeper chatlow~$36No prompt caching — would cut ~70%
Anthropic — cluster naming~$2~$10Working correctly
Railway~$5–10~$25–45Single container
Supabase$25$40–85Must move off Micro
OpenAI embeddingspennies~$1Effectively free
Total~$55–65~$210–270Excluding ad spend

The constraint nobody has checked: Segment's free tier is 1,000 monthly tracked users. At 1,000 MAU you are exactly at the ceiling, and an overage silently drops events into both Amplitude and Customer.io at once — which will present as three unrelated bugs. Confirm the plan tier before the campaign starts.

The order I'd do it in

1
Answer the App Store Connect question — are those 500 codes subscription Offer Codes or cards10 promo codes? One minute, and it decides whether the gift flow ships at all.
2
Ship the work branch. 44 commits. It carries Meta attribution, the ATT prompt, six missing Customer.io traits, push re-registration, all deck instrumentation, the free-user re-read fix, and the 10-pack double-grant fix. Almost everything above is already solved there.
3
Four backend one-liners — gate the signal recompute by type, page /v1/paths, page the nightly user list, fix the 3-vs-10 agent gate. Plus three card_id indexes.
4
Verify events land in Meta Events Manager and Customer.io with a real device before a penny is spent.
5
Then spend — optimising against signup, not purchase, until conversion volume can carry a purchase-optimised ad set.

Your feedback on this page