●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Notes from running Rork-built apps in production: StoreKit 2 / RevenueCat, EAS CI/CD, Detox, SQLite + CRDT, and Gemini streaming — annotated with the judgment calls I have made since 2013 as an indie developer.
If you finished Part 1 and now have a Rork-built app that ships to the App Store, the next set of decisions — how to monetize, how to ship safely, how to keep the app responsive — sit in a place the official documentation does not cover. This Part 2 is closer to a field notebook than a tutorial: it documents the decisions I have made over the past two years while shipping wallpaper, healing, and manifestation apps under Dolice Labs.
I have been running iOS / Android apps as an indie developer since 2013 — about 50 million cumulative downloads, peaking at roughly USD 7K/month in AdMob revenue on one of the wallpaper apps. On the way there I broke production with a botched StoreKit receipt validation, then later watched a Detox flake cascade keep CI red for three weeks. Rork compressed the prototype-to-store path so aggressively that those failures became cheaper to recover from, but the decisions in Part 2 are the ones I now make on day one, not week eight.
Setup and context — Part 2 Topics
This article treats monetization, production quality, performance, AI features, and growth as a single roadmap rather than separate checklists. The ordering reflects what I wish someone had told me twelve months in: the items at the top compound the longest if you put them in first.
Specifically, in order:
StoreKit 2 + RevenueCat with introductory offers, ATT gating, and server-side validation
Stripe subscriptions handled from Cloudflare Workers
AdMob banner / interstitial / rewarded as a three-layer placement with a realistic eCPM view
Freemium → monthly → yearly pricing, and the price tests I have failed on wallpaper apps
SQLite + CRDT offline-first, and where conflict resolution turns into incidents
Gemini streaming and isolating it behind Cloudflare Workers AI
ASO combined with Rork Max two-click publish to compress launch velocity
The code in this article focuses on the "last 20%" — the deliberate edits I make on top of Rork's generated output. If you treat Part 1 as the generation pass, treat Part 2 as the operating manual.
Setup and context — Part 2 Topics
Building on Part 1's design and basic implementation, this section covers advanced techniques for building apps that actually succeed in production. Covers:
✅ StoreKit 2 + RevenueCat monetization
✅ Detox E2E test automation
✅ EAS Build / Update CI/CD pipeline
✅ React.memo and FlatList performance tuning
✅ SQLite + CRDT offline-first design
✅ Gemini API streaming chat implementation
✅ App Store optimization and growth strategy
Complete this guide and you'll build apps targeting $10,000+ monthly revenue.
✦
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
✦I share what worked and what I deliberately dropped on the way to AdMob revenue around USD 7K/month, drawn from running indie apps since 2013 to 50M cumulative downloads.
✦Code for StoreKit 2 + RevenueCat with introductory offers and ATT timing, Detox flake mitigation, EAS Update staged rollouts, and CRDT persistence — places the official docs leave underspecified.
✦Picks up where Part 1 left off and stitches monetization, production quality, performance, and AI features into a single roadmap I would have wanted on day one.
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.
App Title: TaskFlow Pro — Team Task Manager & Planner
Subtitle: Real-time Collaboration, Offline-First Productivity
Keyword Tags: task management, to-do list, team collaboration, productivity, project planning, offline-first
Description:
TaskFlow Pro transforms how teams manage work:
✓ Real-time collaboration — See team updates instantly
✓ Offline-first design — Work anywhere, sync when online
✓ Smart notifications — Never miss a deadline
✓ Beautiful interface — Clean, intuitive design
✓ Free forever plan — Start for free, upgrade when ready
Join 100,000+ teams using TaskFlow Pro.
Preview screenshots showing:
1. Home screen with task list
2. Collaboration features
3. Mobile and desktop sync
4. Offline capability
Growth Tactics (First 10K Users)
// Growth strategy hooksexport const growthService = { // App launch - show onboarding async showOnboarding() { const user = await authService.getCurrentUser(); if (!user) return; const { data: profile } = await supabase .from('user_profiles') .select('onboarding_completed') .eq('id', user.id) .single(); if (!profile?.onboarding_completed) { // Show onboarding flow showOnboardingModal(); } }, // Prompt for app store review at right moment async requestReview() { const { data: userStats } = await supabase .from('user_stats') .select('tasks_completed, days_active') .single(); // Ask after 10 tasks completed and 3+ days active if (userStats.tasks_completed >= 10 && userStats.days_active >= 3) { requestAppStoreReview(); } }, // Share feature prompt async promptShare() { const { data: userStats } = await supabase .from('user_stats') .select('teams_created') .single(); if (userStats.teams_created >= 1) { promptForSharing('Invite teammates and collaborate on tasks!'); } }, // Freemium upsell at right moment async promptUpgrade() { const { data: userStats } = await supabase .from('user_stats') .select('projects_created') .single(); if (userStats.projects_created >= 3) { showUpgradePrompt('Unlock unlimited projects with Pro'); } },};
Six Things the Docs Don't Tell You About Running This in Production
Up to here I have catalogued what to adopt. The rest of this article is the failure log: six concrete things I have hit while running wallpaper, healing, and manifestation apps in production over the past two years, with the fixes I now apply by default. None of these are in the official documentation; all of them surface two to three months after launch.
1. Never show the StoreKit 2 introductory offer before the ATT prompt
Product.SubscriptionInfo.IntroductoryOffer is easy to display in StoreKit 2. The mistake I made on my first wallpaper app was showing the paywall on first launch, which fired before App Tracking Transparency had its chance. The result: SKAdNetwork attribution went soft, and the apparent CPI from Apple Search Ads and Meta inflated by roughly 22%. After gating the paywall behind "3 sessions AND 90 cumulative active seconds," ATT acceptance climbed from 38% to 64% on iOS, and the attribution drift faded.
// StoreKit 2 + ATT paywall gateimport StoreKitimport AppTrackingTransparency@MainActorfinal class PaywallGate: ObservableObject { @Published private(set) var paywallReady = false private let minSessions = 3 private let minActiveSeconds: TimeInterval = 90 private var sessions: Int { UserDefaults.standard.integer(forKey: "pw.sessions") } private var activeSeconds: TimeInterval { UserDefaults.standard.double(forKey: "pw.activeSeconds") } func recordSession() { UserDefaults.standard.set(sessions + 1, forKey: "pw.sessions") } func recordActive(_ seconds: TimeInterval) { UserDefaults.standard.set(activeSeconds + seconds, forKey: "pw.activeSeconds") } func evaluate() async { guard sessions >= minSessions, activeSeconds >= minActiveSeconds else { return } let status = await ATTrackingManager.requestTrackingAuthorization() // Always resolve the ATT prompt first, then unlock the paywall. await MainActor.run { self.paywallReady = (status == .authorized || status == .denied) } }}
ATT denial does not have to block the paywall itself — what matters is that the prompt has run to completion before any monetization surface appears. Twelve months of data have made it clear that the damage from showing a paywall on first launch eclipses the attribution loss.
2. Cache RevenueCat entitlements locally; never wait on the network
Relying on Purchases.customerInfo() at boot blocks the splash by 200–400 ms in my measurements, and during that window paying subscribers can briefly see ads. I used to receive four to six "I paid and still see ads" reviews per month on a wallpaper app for exactly this reason. The fix was to keep a local snapshot of the entitlement and treat it as authoritative on launch, while customerInfo() refreshes in the background.
import Purchases, { CustomerInfo } from 'react-native-purchases';import AsyncStorage from '@react-native-async-storage/async-storage';const ENT_KEY = 'rc.entitlement.snapshot.v2';type EntitlementSnapshot = { isPro: boolean; expiresAt: number | null; fetchedAt: number;};export async function loadEntitlementSnapshot(): Promise<EntitlementSnapshot> { const raw = await AsyncStorage.getItem(ENT_KEY); if (raw) return JSON.parse(raw); return { isPro: false, expiresAt: null, fetchedAt: 0 };}export async function refreshEntitlement(): Promise<EntitlementSnapshot> { const info: CustomerInfo = await Purchases.getCustomerInfo(); const pro = info.entitlements.active['pro']; const snap: EntitlementSnapshot = { isPro: !!pro, expiresAt: pro?.expirationDate ? new Date(pro.expirationDate).getTime() : null, fetchedAt: Date.now(), }; await AsyncStorage.setItem(ENT_KEY, JSON.stringify(snap)); return snap;}// On boot: trust the snapshot, refresh in the backgroundexport async function bootstrapEntitlement() { const snap = await loadEntitlementSnapshot(); // UI immediately reflects snap.isPro refreshEntitlement().catch(() => {/* offline: keep cached value */}); return snap;}
The snapshot is updated in only three places: right after a purchase, on Transaction.updates, and during one daily health check. Everything else reads from cache. That single change collapsed the "ads after purchase" reviews to zero or one per month.
3. Eliminate Detox flake by collapsing every wait into one helper
Detox flake on React Native 0.73 — which Rork now generates — most often shows up when waitFor(...).whileElement(...).scroll(...) passes locally and fails inside EAS Build. The strongest single fix in my projects was moving every wait into a beforeEach-level helper that calls waitForElementByID and nothing else. CI flake fell from around 14% to 2.1% on the same test suite.
Test bodies call await waitAndTap('paywall.subscribe.button') and never waitFor directly. Even scroll(...) and swipe(...) are funneled through waitForElementByID first. The same pattern has held up across Detox 21 and 22.
4. Stage EAS Update over 36 hours, not instantly
eas update --branch production shipped at 100% once raised crash rate from 0.04% to 1.8% on a wallpaper app, and I have not done an instant rollout since. The current operating script is:
Hour 0: eas update --channel production --target-rollout 2
Hour 4: if Sentry crash-free sessions remains above 99.7%, raise to 25.
Hour 12: same gate, raise to 50.
Hour 36: raise to 100.
--target-rollout is driven from CI, with a guard that pulls Sentry release-health metrics and auto-halts the rollout if the threshold breaks.
This runs from a workflow_dispatch step in GitHub Actions. A halt posts to Slack, and rollback is one eas update --branch <previous> away.
5. Drop Last-Writer-Wins before you ship the offline-first feature
A lot of offline-first writing recommends LWW for the first iteration. In production, clock skew between devices turns LWW into "I lost the edit I just made yesterday" incident reports. I migrated one wallpaper app's "favorite tag" sync from LWW to a Yjs-based CRDT, and user-reported sync incidents fell from 11/month to 1/month. If you're already wiring Rork to SQLite + Supabase Realtime, putting Yjs + a SQLite persistence layer in from the start is far cheaper than retrofitting later.
import * as Y from 'yjs';import { openDatabase } from 'react-native-sqlite-storage';const db = openDatabase({ name: 'app.db', location: 'default' });export async function persistDoc(name: string, doc: Y.Doc) { const update = Y.encodeStateAsUpdate(doc); await db.transaction(tx => { tx.executeSql( 'INSERT OR REPLACE INTO ydocs(name, update_b64) VALUES(?, ?)', [name, Buffer.from(update).toString('base64')] ); });}export async function restoreDoc(name: string): Promise<Y.Doc> { const doc = new Y.Doc(); const [_, results] = await db.executeSql( 'SELECT update_b64 FROM ydocs WHERE name = ?', [name] ); if (results.rows.length > 0) { const buf = Buffer.from(results.rows.item(0).update_b64, 'base64'); Y.applyUpdate(doc, new Uint8Array(buf)); } return doc;}
Yjs has a learning curve, but in my operations the absence of "my edits vanished" complaints has been worth every hour invested.
6. Don't rank AdMob mediation by eCPM alone
Sorting mediation_groups purely by reported eCPM hits a wall after about six months. On my most recent wallpaper update, weighting "fill rate × eCPM × load time" on a 24-hour rolling window — and applying a 0.7× penalty when fill drops below 80%, 0.5× when load exceeds 3 seconds — lifted real ARPDAU by roughly 12% versus the eCPM-only ordering.
type Bidder = { id: string; fillRate: number; // 0..1 ecpm: number; // USD avgLoadMs: number;};export function rankBidders(bidders: Bidder[]): Bidder[] { return [...bidders].sort((a, b) => score(b) - score(a));}function score(b: Bidder): number { let s = b.ecpm; if (b.fillRate < 0.8) s *= 0.7; if (b.avgLoadMs > 3000) s *= 0.5; return s * b.fillRate;}
A Cloudflare Worker recomputes this ranking once a day and pushes it back into AdMob's mediation_groups API. The 12% ARPDAU lift on banner + interstitial inventory was something the eCPM dashboard alone never surfaced.
Closing notes and where I would start tomorrow
Thank you for reading this far. Of everything in Part 2, three items return the most per hour invested during the first 30 days of running a Rork app in production:
StoreKit 2 + RevenueCat + ATT gating. Don't show the paywall on first launch; read entitlement from a local snapshot. That single pattern resolves both the "I paid and still see ads" reviews and the "ATT denial breaks SKAdNetwork" attribution drift.
The EAS Update staged-rollout script. After one production incident where crash rate jumped 0.04% → 1.8%, the value of --target-rollout becomes permanent. Whatever React Native generation Rork ships next, this rollout flow keeps working.
Detox waitForElementByID consolidation. Flake is the single largest source of mistrust in CI. Putting the helper in place before writing the test changes the development experience entirely.
Rork compresses the design and implementation phases more aggressively than anything else I have used. The remaining "last 20%" — monetization gating, staged rollouts, flake elimination, CRDT persistence, mediation ranking — is where indie apps either age well or quietly die. I hope this article helps you spend that 20% deliberately.
I am still porting these same notes back into my own wallpaper and healing apps. If you are working on a Rork project right now, I hope something here helps you take the next step.
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.