◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-14Advanced

Building Rork Subscriptions Around RevenueCat Entitlements — Access Checks, Offering-Driven Paywalls, and Restore

Implementation notes for adding subscriptions to a Rork (Expo) app with RevenueCat. Make Entitlements the single source of truth for access, drive the paywall from Offerings so you can change prices remotely, wire up restore and the customer-info listener, and avoid the sandbox traps — all with working code.

Rork611RevenueCat30Subscriptions17Expo234Monetization40

✦ Premium Article

When you finally have a working app out of Rork and decide to add payments, the first place most people stall isn't the SDK call — it's the question of where, and based on what, do I decide whether someone is a paying member? Do you check the purchased product ID, store a receipt, write a "purchased" flag on the device? Leave that vague and you'll pay for it later: change a price and the check breaks, or a user who switched phones writes in saying "I paid and it's gone."

The real value of dropping in RevenueCat isn't that "subscriptions take a few lines." It's that you can anchor your access check to an Entitlement rather than to product IDs. This article assumes the Expo (React Native) app that regular Rork generates, and walks through an Entitlement-centered design, an Offering-driven paywall, restore, and the sandbox issues I actually hit — with working code. I'll spend less time on the product-setup screens and more on the structure that pays off down the road.

Why "check the product ID" falls apart later

The common first move is to use the purchased product ID (com.example.app.pro_monthly) directly as the access check. It works — until operations begin. Start selling monthly and annual and now you if against both. Raise prices by cutting a new product ID and you add code to keep grandfathered subscribers. Name your iOS and Android product IDs differently and the platform branching doubles. Before long, the function that answers "may I unlock Pro?" is a growing list of product-ID comparisons.

RevenueCat's Entitlement abstracts that list away. In the dashboard you create one Entitlement — say pro — and attach "which products grant pro" to it. The app never knows a single product ID; it only asks whether pro is active. Add products or change prices and the attachment is dashboard work; your app code doesn't move. Whether you nail this down first determines, in my experience, how much the next six months of maintenance costs.

Pin the access axis to exactly one thing. "Is the Entitlement active" is the only truth for access; any device flag or product ID is at most a hint. Build the whole codebase on that premise.

Write the service layer around the Entitlement

Install the SDK first. Because billing pulls in native modules, it does not run in Expo Go — you need a development build (expo-dev-client) or EAS Build. Testing in Expo Go and wondering why the purchase call never fires is the very first trap, so I'm putting it up front.

npx expo install react-native-purchases
# Make a dev build — billing does not work in Expo Go
npx expo prebuild
eas build --profile development --platform ios

The package is react-native-purchases (you'll see an older scoped spelling around; this is the current one). Write the service layer as a thin window that returns Entitlement state, so the rest of the app never thinks about "products" or "receipts."

// src/services/purchases.ts
import Purchases, {
  CustomerInfo,
  LOG_LEVEL,
} from 'react-native-purchases';
import { Platform } from 'react-native';
 
// Decide on ONE entitlement for gating. No product IDs appear here.
export const ENTITLEMENT_ID = 'pro';
 
const API_KEY = Platform.select({
  ios: process.env.EXPO_PUBLIC_RC_IOS_KEY ?? '',
  android: process.env.EXPO_PUBLIC_RC_ANDROID_KEY ?? '',
}) as string;
 
export async function configurePurchases() {
  if (__DEV__) Purchases.setLogLevel(LOG_LEVEL.DEBUG);
  // Without an appUserID, RevenueCat assigns an anonymous one.
  // If you have your own auth, the right move is logIn() AFTER sign-in,
  // not passing an ID to configure() — that double-runs the anon→known alias.
  await Purchases.configure({ apiKey: API_KEY });
}
 
// The single access check. Nothing else decides premium access.
export function isPro(info: CustomerInfo): boolean {
  return info.entitlements.active[ENTITLEMENT_ID] !== undefined;
}
 
export async function getCustomerInfo(): Promise<CustomerInfo> {
  return Purchases.getCustomerInfo();
}

The key is that isPro takes a CustomerInfo and holds no internal state — it's a pure function. If your check depends on some global variable, the value drifts between "just purchased," "just restored," and "just launched." Always phrase it as "ask, passing the freshest CustomerInfo I have," and it lines up cleanly with the listener below.

✦

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
✦Use a single Entitlement as the only access check so price changes and platform differences don't break gating
✦An Offering-driven paywall that reads packages remotely instead of hardcoding prices
✦The restore flow, the CustomerInfo listener, and the sandbox issues that actually cost you time
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.

or
Unlock all articles with Membership →
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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Business2026-04-04
Rork App Revenue Design: Where Ads and Subscriptions Eat Each Other
How to layer AdMob, subscriptions, and one-time purchases in a Rork app without letting them cannibalize each other. Covers value-density placement, entitlement-first RevenueCat architecture, server-side receipt verification, and behavior-triggered paywalls.
⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.