Goal: a premium subscription that works day one in the Eurozone, UK, Australia, USA, Canada and (phased) LATAM — with renewals, cancellations and refunds handled by proven rails, not by us. Decision paper for founders; nothing is built yet.
Rule 1 — In-app digital subscriptions must use the stores. Apple and Google require that digital goods sold inside the native apps go through In-App Purchase / Play Billing (15% fee up to $1M/yr via the small-business programs, 30% above). A Stripe checkout inside the iOS app for a subscription is a rejection. This is not a choice between "AWS shopping cart or others" — on mobile the rail is imposed.
Rule 2 — The web is free territory. On nutrisynccollective.com we can charge with any processor at ~2-3% instead of 15-30%. The classic play (Spotify model): sell on both, steer nothing illegally, and let the web be the margin-friendly door. The app can honor a web-purchased subscription without ever mentioning where to buy.
Corollary: we will run two sales rails with one source of truth for "is she premium?".
(Why not AWS: Amazon has no consumer cart/subscription product for this use case — Amazon Pay is a wallet button, not a billing engine. The AWS wave in our backlog is about compute, not checkout.)
iOS app ──── StoreKit (Apple IAP) ──┐
Android ──── Play Billing ──────────┤ webhooks ┌──────────────────┐
├──► RevenueCat ────► │ Supabase │
Web ──────── Stripe Billing or ─────┘ (subscription │ subscriptions tbl │
Paddle (MoR) state hub) │ + is_premium() │
│ RLS gates │
└──────────────────┘
subscriptions table (user_id, store, product, status, current_period_end, will_renew) updated by RevenueCat webhooks (Edge Function), plus a SQL helper is_premium(uid) used by RLS/RPCs to gate premium features. The app never trusts itself — it asks the backend.| Region | Mobile (IAP) | Web |
|---|---|---|
| Eurozone, UK, USA, Canada, Australia | Native — stores localize price & currency per storefront, handle taxes | Stripe/Paddle native; cards + Apple Pay/Google Pay buttons |
| LATAM (phase) | IAP works everywhere the stores operate; store handles local pricing tiers | Cards work cross-border, but LATAM converts on local methods (PIX in Brazil, OXXO/SPEI in Mexico, local wallets). Launch LATAM web = cards only; add a local processor (dLocal / EBANX / Mercado Pago) only when the LATAM cohort is real |
Pricing: use Apple/Google price tiers per storefront and Stripe/Paddle multi-currency price lists — one product, localized price points (LATAM sensitivity: plan regional tiers, not FX conversion).
Offer: Free tier (current app) + NutriSync Premium — monthly and annual (annual with ~35-40% saving badge) + 7-day free trial. Exact prices = founders + tracker assumptions (doc G3).
A · Becoming a paying user (app): Onboarding stays free (never paywall registration — health apps die there). Paywall appears at value moments: the yearly Progress view (already a locked card today), advanced insights, and a soft card after the first completed cycle. Tap → Paywall screen → plan choice → native store sheet (Face ID confirms — note the elegance: the biometric decision from doc 28 composes with this) → receipt lands in RevenueCat → webhook flips subscriptions → premium unlocks live, all surfaces at once.
B · Becoming a paying user (web): marketing pricing section (exists today, currently waitlist-steered) → checkout hosted by Stripe/Paddle (card, Apple/Google Pay) → webhook → same table. She opens the app: already premium. One subscription per account enforced by the hub (RevenueCat blocks double-billing across stores).
C · Renewals: stores/processor auto-charge; webhook refreshes current_period_end. Failed card on web → dunning emails + grace period (7 days premium while retrying — standard, avoids rage-churn). App shows a quiet "billing issue" banner only in Settings.
D · Cancellation: never a support ticket. In-app "Manage subscription" deep-links to the store's own management sheet (Apple/Google requirement); web portal (Stripe Customer Portal / Paddle equivalent) for web subs. Cancelling = will_renew=false, premium runs to period end, then graceful downgrade (data never deleted — free tier keeps her history, premium features re-lock). Refunds & EU 14-day withdrawal: handled by the store/MoR per their policy; we honor state changes via webhook.
New surface inventory (added to doc 12 as §22):
Hard prerequisite: the web rail (Stripe/Paddle) needs the company + bank account (doc 19) — it cannot start on a personal basis. Apple/Google IAP can start on the current individual account (banking + tax forms in App Store Connect) and transfers with the app later.
| Phase | Contents | Depends on |
|---|---|---|
| P0 — foundation (can start now) | subscriptions schema + is_premium() + RLS gating + paywall/locked/Settings UI behind a feature flag + RevenueCat account & SDK wiring |
nothing |
| P1 — mobile live | IAP products in both stores, sandbox → TestFlight testing, prices per storefront | P0 + banking/tax forms in ASC |
| P2 — web live | Paddle-or-Stripe checkout + portal + webhooks | company registered |
| P3 — LATAM depth | local methods via dLocal/EBANX/MP, regional price tiers review | real LATAM cohort |
Effort: P0 ≈ 2 sessions · P1 ≈ 2 sessions + store review cycles · P2 ≈ 2 sessions. All gated behind a flag until founders flip it.