●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking
Why Subscription Apps Fail—and How to Build a Data-Driven Monetization System with Rork, RevenueCat, PostHog, and Superwall
A complete guide to integrating RevenueCat, PostHog, and Superwall into your Rork app to maximize subscription revenue. Includes production-ready code for churn prediction, paywall A/B testing, and personalized retention notifications.
Three months after launching on the App Store with a subscription model, and the revenue is still barely covering your monthly coffee budget.
The features work. The design looks clean. The pricing seems reasonable. And yet, the numbers won't budge.
For most indie developers building subscription apps with Rork, the real problem isn't the product itself—it's that they have no idea why users aren't converting, or why paying users are quietly canceling. Pricing is set by gut feel. Paywalls are placed by intuition. Push notifications go out on a schedule someone copied from a blog post.
This guide walks you through building a system where those decisions are informed by data—specifically, by connecting four tools: Rork + RevenueCat + PostHog + Superwall. Not as individual tools, but as an integrated pipeline that detects churn risk before it becomes cancellation, and intervenes automatically.
Why Most Subscription Apps Plateau Within a Year
The patterns are consistent enough that they can be grouped into four buckets.
Pattern 1: A paywall that just sits there
Most apps deploy one paywall design and never change it. The same screen is shown to a first-time user on launch day and to someone who's been using the app daily for two weeks. But those two users are in completely different mental states—what convinces one to subscribe won't convince the other. Without tooling to test different approaches, you're locked into whatever you shipped first.
Pattern 2: Cancellation data without cancellation reasons
RevenueCat alone tells you how many users canceled this month. It doesn't tell you why, or what those users were doing in the app before they hit cancel. PostHog integration fills that gap—once you can compare the in-app behavior of users who retained versus those who canceled, patterns emerge quickly.
Pattern 3: Missing churn signals
Cancellations rarely happen spontaneously. In the 7–14 days before someone cancels, you typically see declining open frequency, reduced usage of core features, and occasional visits to settings or pricing pages. These signals are detectable and actionable—if you're collecting the right events.
Pattern 4: Optimization without measurement
Changing a paywall headline or adjusting a price point without a controlled test makes it impossible to know what actually moved the needle. Superwall enables parallel A/B tests across paywall variants while tracking conversion rates against a shared baseline.
System Architecture: What Each Tool Does
Before writing a line of code, it's worth being precise about roles.
RevenueCat manages subscription state and purchase history, abstracting away the differences between StoreKit 2 (iOS) and Google Play Billing (Android)
PostHog records user behavior as events and makes them queryable through funnels, cohorts, and session replays. When RevenueCat subscription state is synced to PostHog user properties, you can directly compare the behavior of paying versus free users
Superwall owns paywall presentation and A/B testing. It can trigger different paywall variants based on user conditions—session count, feature usage, days since install—rather than serving the same screen to everyone
Rork is the React Native app that ties these three together. The generated Rork code receives the SDK integrations added in the steps below
When these four work together, you get a loop: PostHog detects churn signals → RevenueCat confirms subscription state → if non-subscriber, Superwall shows a targeted paywall; if subscriber, a retention push notification fires with a personalized discount.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Diagnose the root cause of flat subscription revenue (data blindness) and get a complete system design for fixing it with RevenueCat × PostHog integration
✦Production-ready code examples let you start paywall A/B testing, churn prediction push notifications, and LTV cohort analysis today
✦Learn the design patterns that indie developers use to achieve $500+/month in recurring revenue with Rork, including real failure stories and pitfalls
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Entitlement identifiers are hard to change once in production—plan for them.
// src/services/revenuecat.tsimport Purchases, { PurchasesPackage, CustomerInfo, LOG_LEVEL } from 'react-native-purchases';import { Platform } from 'react-native';const REVENUECAT_API_KEY = Platform.OS === 'ios' ? process.env.EXPO_PUBLIC_RC_IOS_KEY\! : process.env.EXPO_PUBLIC_RC_ANDROID_KEY\!;export async function initializeRevenueCat(userId: string): Promise<void> { try { if (__DEV__) { Purchases.setLogLevel(LOG_LEVEL.DEBUG); } await Purchases.configure({ apiKey: REVENUECAT_API_KEY }); // Critical: use the same userId as PostHog's distinct_id await Purchases.logIn(userId); console.log('RevenueCat initialized for user:', userId); } catch (error) { console.error('RevenueCat initialization failed:', error); throw error; }}export async function getCustomerInfo(): Promise<CustomerInfo | null> { try { return await Purchases.getCustomerInfo(); } catch (error) { console.error('Failed to fetch customer info:', error); return null; }}export async function hasProAccess(): Promise<boolean> { const customerInfo = await getCustomerInfo(); if (\!customerInfo) return false; return customerInfo.entitlements.active['pro'] \!== undefined;}
The key decision here: the userId passed to Purchases.logIn must match the distinct_id used in PostHog. This single ID links purchase history to behavioral data.
Step 2: PostHog Integration and RevenueCat Sync
For PostHog's basic setup, see the PostHog Product Analytics Guide. This section focuses on syncing subscription state from RevenueCat into PostHog user properties.
For Superwall's basic setup, refer to the Superwall Paywall A/B Testing Guide. Here we build context-aware paywall delivery based on user behavior state.
Step 4: Churn Prediction and Automated Intervention
This is where the system starts earning its keep. Most churn is predictable, and most predicted churn is preventable.
Calculating a Churn Risk Score
// src/services/churnPrediction.tsimport AsyncStorage from '@react-native-async-storage/async-storage';import { trackEvent } from './tracking';interface UserBehavior { lastOpenDate: string; totalOpenCount: number; coreFeatureUsageCount: number; daysSinceLastCoreFeatureUse: number; paywallViewCount: number; helpPageViewCount: number;}// Returns a churn risk score from 0–100export function calculateChurnRiskScore(behavior: UserBehavior): number { let score = 0; // Days since last open (up to 40 points) const daysSinceLastOpen = Math.floor( (Date.now() - new Date(behavior.lastOpenDate).getTime()) / (1000 * 60 * 60 * 24) ); if (daysSinceLastOpen >= 7) score += 40; else if (daysSinceLastOpen >= 3) score += 20; else if (daysSinceLastOpen >= 1) score += 5; // Days since last core feature use (up to 30 points) if (behavior.daysSinceLastCoreFeatureUse >= 14) score += 30; else if (behavior.daysSinceLastCoreFeatureUse >= 7) score += 15; else if (behavior.daysSinceLastCoreFeatureUse >= 3) score += 5; // Help page views—a signal of frustration (up to 15 points) if (behavior.helpPageViewCount >= 3) score += 15; else if (behavior.helpPageViewCount >= 1) score += 8; // Paywall views—a signal of price hesitation (up to 15 points) if (behavior.paywallViewCount >= 3) score += 15; else if (behavior.paywallViewCount >= 1) score += 7; return Math.min(score, 100);}export async function evaluateChurnRisk(userId: string): Promise<void> { try { const behaviorData = await AsyncStorage.getItem(`user_behavior_${userId}`); if (\!behaviorData) return; const behavior: UserBehavior = JSON.parse(behaviorData); const riskScore = calculateChurnRiskScore(behavior); trackEvent('churn_risk_evaluated', { risk_score: riskScore, risk_level: riskScore >= 70 ? 'high' : riskScore >= 40 ? 'medium' : 'low', }); if (riskScore >= 70) { await triggerChurnInterventionNotification(userId, riskScore); } } catch (error) { console.error('Churn risk evaluation failed:', error); }}async function triggerChurnInterventionNotification( userId: string, riskScore: number): Promise<void> { const sentKey = `churn_notification_sent_${userId}`; const alreadySent = await AsyncStorage.getItem(sentKey); if (alreadySent) { const sentDate = new Date(alreadySent); const daysSinceSent = Math.floor( (Date.now() - sentDate.getTime()) / (1000 * 60 * 60 * 24) ); if (daysSinceSent < 7) return; } try { await fetch(process.env.EXPO_PUBLIC_API_URL + '/api/send-retention-push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, notificationType: 'churn_prevention', riskScore, }), }); await AsyncStorage.setItem(sentKey, new Date().toISOString()); trackEvent('churn_intervention_notification_sent', { risk_score: riskScore, }); } catch (error) { console.error('Failed to send churn notification:', error); }}
Step 5: App Initialization
// src/hooks/useAppInitialization.tsimport { useEffect, useState } from 'react';import { initializeRevenueCat } from '../services/revenuecat';import { trackAppOpen, syncSubscriptionStateToPostHog } from '../services/analytics';import { evaluateChurnRisk } from '../services/churnPrediction';import Superwall from '@superwall/react-native-superwall';import { useAuth } from './useAuth';export function useAppInitialization() { const { userId } = useAuth(); const [isReady, setIsReady] = useState(false); const [error, setError] = useState<Error | null>(null); useEffect(() => { if (\!userId) return; async function initialize() { try { await initializeRevenueCat(userId); Superwall.configure({ apiKey: process.env.EXPO_PUBLIC_SUPERWALL_KEY\!, }); await trackAppOpen(userId); // Run churn evaluation asynchronously—don't block startup evaluateChurnRisk(userId).catch(console.error); setIsReady(true); } catch (err) { console.error('App initialization failed:', err); setError(err instanceof Error ? err : new Error(String(err))); // Don't block the app from loading on initialization failure setIsReady(true); } } initialize(); }, [userId]); return { isReady, error };}
Common Mistakes and Pitfalls
Pitfall 1: Mismatched User IDs Between RevenueCat and PostHog
This is the most common integration error. If Purchases.logIn receives a different identifier than posthog.identify, purchase history and behavioral data become impossible to correlate.
// ❌ Wrong: two different IDsawait Purchases.logIn(auth.uid); // Firebase UIDposthog.identify(deviceId); // Device ID// ✅ Correct: one consistent ID across bothconst userId = auth.uid;await Purchases.logIn(userId);posthog.identify(userId);
Pitfall 2: Calling Superwall's register on Every Render
Placing showContextualPaywall in the JSX body rather than a useEffect causes the paywall to appear on every re-render.
// ❌ Wrong: called on every renderfunction FeatureScreen() { showContextualPaywall('feature_accessed', {...}); return <View>...</View>;}// ✅ Correct: called once on mountfunction FeatureScreen() { useEffect(() => { showContextualPaywall('feature_accessed', {...}); }, []); return <View>...</View>;}
Pitfall 3: Sending Too Many Retention Notifications
Without throttling, multiple churn-prevention notifications in a short window often trigger uninstalls rather than re-engagement. The 7-day cooldown in the code above is a minimum. More importantly, message content matters more than timing—a notification referencing the specific feature the user last used converts at 2–3x the rate of a generic "We miss you!" message. For the personalization implementation, see the AI Churn Prediction and Push Notification Guide.
Three PostHog Dashboards Worth Building
Once the instrumentation is in place, these three views deliver the most immediate value.
Dashboard 1: Paywall-to-Purchase Funnel
Track paywall_viewed → purchase_initiated → purchase_completed. Drop-off between the first two steps indicates a paywall design or messaging problem. Drop-off between the second and third steps usually indicates a price objection.
Dashboard 2: Churn Risk Score Distribution
Plot the daily churn_risk_evaluated scores as a histogram. If more than 20% of active subscribers are scoring above 70, there's a systemic product issue to investigate—not just individual user behavior.
Dashboard 3: LTV by Install Cohort
Group users by install week and track cumulative revenue per cohort. If newer cohorts show lower 30-day LTV than earlier cohorts, it can signal a product quality decline, increased competition, or that your user acquisition is reaching a less-engaged audience segment.
Production Checklist
A few things to verify before shipping to production.
Privacy policy update: if PostHog is collecting behavioral data, the privacy policy needs to describe what's collected and why. App Store reviewers check for this.
Test environment separation: RevenueCat's Sandbox and a PostHog test project should be distinct from production. Use the __DEV__ flag to switch environment variables.
Parallel initialization: calling RevenueCat, Superwall, and PostHog sequentially during cold start adds latency. Promise.allSettled handles them in parallel, and ensures a failure in one SDK doesn't block the others.
Where to Go From Here
The full system described here takes roughly half a day to wire together for the first time. But once it's running, you move from "I think the $4.99/month price point might be too high" to "users who reach the paywall on session 4 convert at 3.2x the rate of users who see it on session 1"—and that difference is what compounds into meaningful revenue growth over time.
If you're starting from zero, add PostHog first. Just knowing your paywall-to-purchase conversion rate changes the next decision you make about the product. Once data is flowing, layer in Superwall for controlled testing. The churn prediction system can come last—it needs a few weeks of behavioral data before the risk scores become meaningful anyway.
The goal isn't to have sophisticated tooling. The goal is to stop guessing.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.