- Goal: unify Stripe payment intents (web) and RevenueCat (mobile IAP) so subscribers are stored, verified, and fulfilled through the same payment service entry point at
packages/api/src/services/payment/stripe.service.ts. - Scope: backend service layer, webhook handling, Supabase persistence, env/config wiring. Client changes (Expo/Next) are limited to calling the new API contracts that this plan introduces.
- Out of scope: pricing strategy, UI redesign, or migrating historical transactions (capture them via a one-time sync instead).
stripe.service.tsexposes a typedcreatePaymentSheet+confirmPaymentflow that tags PaymentIntents with the RevenueCat product/entitlement metadata.- New RevenueCat service + webhook keeps Supabase
subscriptions(or an equivalent table) in sync for Apple/Google receipts and Stripe payments. - API returns a normalized
Entitlementobject that downstream apps can trust regardless of purchase surface. - Observability: structured logs + metrics for payment creation, webhook processing, entitlement grants/revocations, and error bubbling through tRPC.
packages/api/src/services/payment/stripe.service.tsonly wires upcreatePaymentSheet, creates a fresh Stripe customer each call, and never persists payment metadata in Supabase.- No shared SKU catalog; RevenueCat is not referenced anywhere in the repo.
- No webhooks or background jobs to reconcile status between Stripe, RevenueCat, and Supabase.
- Shared product catalog module (JSON or TS constant) describing SKU id, platform (Stripe, Apple, Google), RevenueCat entitlement id, billing interval, and price.
- Hardened Stripe service with helpers (
getOrCreateCustomer,upsertPaymentRecord,attachEntitlementMetadata) and no non-null assertions. - New RevenueCat service + webhook handler to validate signatures, translate events to internal entitlement operations, and persist to Supabase via
packages/api/src/services/payment. - Supabase schema additions (if missing):
subscriptions,subscription_events, and optionalpayment_methodstables + triggers to keep audit log. - Documented runbooks + testing checklist inside this plan.
- Inventory paid offerings (support tiers, credits, etc.) with Product + Entitlement IDs in RevenueCat dashboard.
- Create
packages/api/src/services/payment/catalog.tsexporting a typed array of products (use discriminated unions instead ofany). - Update
createPaymentSheetcaller to pass aproductIdand compute amount/currency from the catalog instead of trusting client-provided values. - Document mapping (Stripe price id ↔ RevenueCat entitlement id) inside the catalog and keep it as the single source of truth.
- Add to
.env.exampleand deployment secrets:REVENUECAT_API_KEY,REVENUECAT_WEBHOOK_SECRET,STRIPE_PRICE_<SKU>, and any publishable keys the clients need. - Extend
packages/api/src/config/payment.ts(create if missing) to read + validate these variables via Zod; surface typed config to services. - Ensure secrets are loaded in Supabase Edge functions (if used) and CI to keep parity.
- Refactor
createPaymentSheetto:- Reuse an existing Stripe customer (query Supabase by
stripe_customer_id; create + persist only when absent). - Require a
productId, derive amount/currency/description from the catalog, and stash the RevenueCat entitlement id insidemetadata. - Return strongly typed values; remove
!non-null assertions, using explicit error guards instead.
- Reuse an existing Stripe customer (query Supabase by
- Add helpers:
getOrCreateCustomer({ supabase, stripe, userId })createPaymentIntent({ product, customerId, metadata })upsertPaymentRecord({ supabase, paymentIntent, source: 'stripe' })
- Expose a
handleStripeWebhookfunction that validates signatures, persists events, and forwards successful payments to the RevenueCat sync service (step 4). - Write unit tests around failure paths (customer creation, payment intent errors, webhook signature failures) using jest + Stripe mock client.
- Create
packages/api/src/services/payment/revenuecat.service.tsto:- Fetch customer info using RevenueCat REST API (with API key and app user id ==
userId). - Normalize webhook payloads (initial purchase, renewal, cancellation, billing issues) into an internal
EntitlementEventtype. - Call shared entitlement helpers to upsert Supabase records and invalidate profile cache where needed.
- Fetch customer info using RevenueCat REST API (with API key and app user id ==
- Add
/api/webhooks/revenuecat(tRPC procedure or Next.js route) that verifiesRevenuecat-Signature, deduplicates events, and publishes to a queue (Supabase channel or background job) if processing may be slow. - Ensure the service can reconcile Stripe-originated purchases by looking up
stripe_customer_idfrom metadata stored on PaymentIntents (step 3).
- Create migrations for
subscriptions(user_id, source, product_id, entitlement_id, status, period_end, stripe_customer_id, revenuecat_app_user_id) andsubscription_events(for auditing). - Add indexes on
user_id,entitlement_id, andsource+statusto support queries. - Consider a scheduled job (Edge Function cron) to reconcile entitlements by calling RevenueCat REST for active users and verifying Stripe subscription status in case webhooks fail.
- Run
yarn supa generateafter migrations to refresh types and update any affected services.
- Add a
paymentRouterprocedure to fetch the normalized entitlement snapshot for the authenticated user (reads from Supabase, not Stripe/RevenueCat directly). - Update Expo/Next clients to:
- Request
productIdfrom the backend catalog or remote config. - Use RevenueCat SDK (native) for in-app purchases and send receipt/app user id to backend for verification.
- Use Stripe PaymentSheet (web) via the existing
createPaymentSheetendpoint, but now provide theproductIdonly.
- Request
- Handle success/failure states consistently and surface entitlement status in UI once backend confirms.
- Standardize logs (using existing logger utility) with
payment_source,product_id,entitlement_id,user_id, andevent_type. - Emit metrics (Datadog/PostHog) for webhook success/failure counts, entitlement sync duration, and Stripe/RevenueCat API latency.
- Add alerting for stuck subscriptions (no renewal event seen within 2x billing cycle) and webhook signature failures.
- Unit tests: Stripe + RevenueCat services (mock SDK/HTTP), catalog helpers, Supabase upsert logic.
- Integration tests: Simulate end-to-end purchase via mocked Stripe PaymentIntent + RevenueCat webhook to ensure entitlements flip to
active. - Manual QA:
- iOS/Android purchase through RevenueCat sandbox → confirm entitlement in Supabase + UI.
- Web card purchase → confirm PaymentIntent metadata, webhook, and entitlement sync.
- Cancellation/refund flows on both providers update status to
canceled/paused.
- Deploy behind a feature flag (
payments_unified_v1). Gate both API responses and client UI to avoid partial rollout. - Run parallel logging (old vs new) for a few beta testers to ensure entitlements stay in sync.
- Once metrics show healthy flow for 24h, remove the flag and update documentation (CLAUDE*.md, onboarding guides).
- Retrospect after launch: capture lessons in
docs/payments.mdand schedule follow-up tasks (refund automation, analytics instrumentation) if needed.
- Do we already have a Supabase table for subscriptions, or should the migration introduce one?
- Are there legacy PayPal or App Store Server API flows that also need to emit RevenueCat entitlements?
- Should customer support tooling live in Supabase (SQL scripts) or in an internal admin UI to manually grant entitlements?