Answer
Every payment, from Stripe or the App Store and Play via RevenueCat, ends as one row per parent in user_subscriptions. Access is computed from that row at read time. Most money-losing bugs are places where a webhook is acknowledged with 200 but nothing is written.
- Two rails feed one row: Stripe for US parents on iOS, RevenueCat for everyone else and all Android.1
- The RevenueCat webhook maps
app_user_idtoparent_profile.id; an anonymous id is acked and dropped.2 user_subscriptionshasUNIQUE(parent_id), so every event from either rail overwrites the same row.3- On 18–21 Oct, 20 paid App Store and Play purchases were dropped this way.
Applies toBranch dev-navaratribackend 5fb3968mobile 26f25f13read-only analysis, 27 Oct 2026
How does money reach the database?
Diagram structure
- Parent App
app - Billing route
rail - Store or Stripe
store - Webhook router
hook - Billing services
apply - user_subscriptions
row - Membership check
gate app→rail· countryapply→row· upserthook→apply· parent foundrail→store· payrow→gate· readstore→hook· event
- Parent App
The app asks the backend which rail to use, using the store's storefront country.
- Billing route
US on iOS can pick Stripe; everyone else and all Android use RevenueCat.
- Webhook router
Stripe events are signature-checked; RevenueCat events use a Bearer secret.
- Billing services
The parent is resolved, guards run, then the single row is upserted.
- Membership check
Access is derived from status, is_trial and period end on every read.
Which page answers which question?
| Question | Page |
|---|---|
| What are the parts and how do they connect? | 01-system-overview.html |
| What happens inside the RevenueCat webhook? | 02-revenuecat-webhook.html |
| Which states can a subscription be in? | 03-subscription-lifecycle.html |
| Which tables and keys hold payment data? | 04-payments-data-model.html |
| How does an App Store or Play purchase flow? | 05-iap-purchase-sequence.html |
| How does Stripe checkout flow? | 06-stripe-checkout-sequence.html |
| What happened on 18–21 Oct? | 07-incident-timeline.html |
| Every bug with file and line | ../../payment-bug-analysis-2026-10-27.md |
All pages sit in this folder. Each diagram node links to its pinned source lines.
Where can a paid event be lost?
| Bug | Rail | Effect |
|---|---|---|
Anonymous app_user_id acked as ghost parent | RevenueCat | P0Payer gets nothing |
| Claim written before apply; failed release blocks retries | RevenueCat | P0Event lost after a DB error |
| Plan lookup ignores retired plans | RevenueCat | P0Renewals skipped, access lapses |
| No transaction or timestamp check on events | RevenueCat | P0Old event expires a newer subscription |
| Active RevenueCat check ignores period end | Stripe | P0Stripe bills, nothing stored |
| Restore runs while RevenueCat is anonymous | App | P0Receipt moves to an anonymous id |
| Config error cached as no parent for 5 minutes | App | P1Purchases go anonymous |
subscription.updated applied without re-fetch | Stripe | P1Late event rolls a paid row back |