You are adding a paywall to a Flutter app, and the screen is the easy part. The hard part is the single line that asks "is this person allowed?", because by launch it gets called from forty places and every one of them has to agree. Your first action: search your codebase for where you check access today, and count the distinct places.
A Flutter RevenueCat subscription is two systems wearing one coat: a store transaction owned by Apple or Google, and an entitlement your app decides to trust. The bugs live at the seam between them. My own BookBed property management SaaS bills through Stripe rather than in-app purchase, so I did not write this from a RevenueCat production account of my own. I wrote it from RevenueCat's documentation, package changelog and issue tracker, all read on 30 September 2026, because these rules move.
Below: what RevenueCat replaces, how to gate features so pricing can change without a migration, what happens offline, a race between webhook and client that is still an open GitHub issue, and the order I would ship it in.
What Does RevenueCat Give You Over in_app_purchase?
RevenueCat replaces receipt validation, cross-platform subscription state and the product-to-entitlement mapping that the in_app_purchase plugin leaves to your own backend. The rest of what it sells is analytics and experiments layered on that core.
The purchases_flutter package was at version 10.13.2 on pub.dev when I checked on 30 September 2026, published six days earlier. Here is how the options compare.
| Concern | in_app_purchase | RevenueCat | Roll your own |
|---|---|---|---|
| Store transaction plumbing | Yes | Yes | You write it |
| Server-side receipt validation | You build it | Included | You build it |
| Product to entitlement mapping | None | Dashboard-configured | Your database |
| Cross-platform subscriber identity | None | app_user_id | Your auth system |
| Cost | Free | Free up to $2,500 MTR, then 1% | Engineering time |
The 1% deserves a second look before you commit. RevenueCat's pricing page says you pay nothing up to $2,500 in monthly tracked revenue, then 1% of what you track. The page does not spell out in the text I could read whether that revenue is counted before or after the store's cut, so confirm the definition with RevenueCat before you model it. At small scale the fee is noise. At tens of thousands in tracked revenue it is a line item you should have modelled.
One migration hazard matters before you add the dependency. According to the purchases_flutter changelog, version 10.0.0 moved to Google Play Billing Library 8.3.0, raised the minimum Android SDK from 21 to 23, and removed the workaround that let consumed one-time products be restored. The changelog warns that if your non-consumables are configured as consumables in the dashboard, customers cannot restore them from 10.0.0 onward. Check that mapping first.
How Do You Gate Features on RevenueCat Entitlements?
Call Purchases.getCustomerInfo() and test whether your entitlement, such as pro, is active, and never branch on a product identifier: the entitlement lets any number of products grant the same access.
An entitlement is a named capability in the RevenueCat dashboard. Gate on it and you can add a Black Friday product, a regional price or a lifetime tier without shipping a build. Gate on a product ID and every pricing experiment becomes an app update.
Future<bool> hasPro() async {
final info = await Purchases.getCustomerInfo();
return info.entitlements.all['pro']?.isActive ?? false;
}
This mirrors the pattern in RevenueCat's quickstart, which checks entitlements.all[...] for isActive. The same page notes that the SDK caches the latest CustomerInfo whenever it updates, so in most cases the call pulls from the cache.
There is one place where product IDs legitimately appear in client code: the paywall itself, where you render an Offering and need prices. Even there, read the offering's packages so the dashboard stays in charge. The rule is narrower than "never". No product ID should sit inside a conditional that guards a feature.
If you sync entitlement state into a Firebase backend, the mechanics are in the Flutter, Firebase and RevenueCat stack guide.
What Happens to a Flutter Subscription When the Device Goes Offline?
The SDK serves the latest cached CustomerInfo, so a paying customer on a plane normally still passes your entitlement check without a network round trip.
The harder case is RevenueCat's own servers being unreachable. RevenueCat's Offline Entitlements announcement describes SDKs that keep processing purchases during an outage and sync when connectivity returns. On iOS it verifies current subscriptions through StoreKit 2, which means iOS 15.0 or later.
The same post lists limits. A fresh install needs one online session first, cross-platform purchases do not sync during an outage, consumable purchases are not supported offline, and subscriptions and non-consumables work offline on iOS only. Read the post again before you promise offline access on Android.
There is also a webhook you probably have not handled. Per the webhook event types documentation, RevenueCat sends TEMPORARY_ENTITLEMENT_GRANT when it cannot validate a purchase with the store, granting access for at most 24 hours. It later resolves into an INITIAL_PURCHASE or an EXPIRATION. If your backend books every grant event as a confirmed sale, you will record revenue that disappears a day later.
Does the Webhook Arrive Before the Client Sees the Entitlement?
Not necessarily, and nothing in the quickstart promises it. The client can hear about the purchase before your server does.
Issue #1094 on purchases-flutter is open and labelled as an enhancement. The reporter asks whether the webhook can be guaranteed to complete before the client is told about a new entitlement, because otherwise the app may call a server method that the server thinks the user should not have access to. As of 30 September 2026 that request is open, so I treat the ordering as unguaranteed.
That suggests a rule: split the trust. Client entitlement state is for rendering, such as hiding the upgrade button and showing the pro tab. Server-side entitlement state is for anything that costs money or exposes data, such as API quota, storage or a model call. Do not let the client's optimism authorise the server's spend, and design the server to answer "not yet" with a retry rather than a hard failure.
BookBed shows the same shape with a different vendor. Its case study describes Stripe checkout across browser tabs, using BroadcastChannel with a Firestore fallback so the original tab learns the outcome. Two systems on different clocks always force you to decide which one is allowed to be wrong for a few seconds.
Should you build restore purchases first?
Apple's App Review Guidelines say in section 3.1.1 that you should make sure you have a restore mechanism for any restorable in-app purchases. Treat it as required on iOS. Add one button that calls Purchases.restorePurchases() somewhere a frustrated person can find it fast, not buried three screens deep.
What does Family Sharing do to your metrics?
RevenueCat's Apple Family Sharing documentation says family-shared transactions are not included in charts data and are excluded from most overview metrics, with New Customers and Active Customers as the exceptions. It also notes a deliberate one-hour delay before a new shared subscription reaches family members, and that entitlements from family membership are revoked when a user leaves the family group.
Webhook payloads carry an is_family_share field, which the event documentation says is always false outside the App Store. If active customers climb while revenue looks flat, check that flag before you rewrite onboarding.
How should you design the paywall screen?
Show one screen after onboarding with free and paid side by side, one highlighted plan, and a button that names the outcome rather than the transaction. Where you place it matters more than how it looks.
The strategic choice sits above the design. RevenueCat's State of Subscription Apps 2026 report, covering more than 115,000 apps and $16 billion in revenue, reports a median Day-35 trial-to-paid conversion of 10.7% for hard paywalls against 2.1% for freemium, roughly a fivefold gap. It is a correlation across apps, not proof that switching your paywall type will produce that lift. A hard paywall also trades install volume for conversion rate, so model both before choosing.
For layout work, the Flutter developer tools I keep installed cover the debugging setup. Across the FlutterFlow marketplace templates I maintain, the sensible design choice is to keep the layout plain and make the trigger configurable, since placement is the variable you will want to change.
In what order would you ship it?
- Configure products in App Store Connect and Google Play Console first and get them into a reviewable state. Everything downstream blocks on this.
- Create entitlements in the RevenueCat dashboard, then attach products to them. Name entitlements after capabilities such as
pro, never after price points. Use plain ASCII: a lookalike Unicode character in an identifier will not match. - Add
purchases_flutterand callPurchases.configureonce at startup, before any UI that reads entitlement state. - Log in your own user identifier with
Purchases.logInso the subscriber follows the account across devices. - Write a single
hasEntitlement(String key)helper and route every gate through it, so there is one place to add logging. - Add restore purchases to settings and test it on a device that has never run the app.
- Point the webhook at your backend, verify each delivery using the method in RevenueCat's webhook documentation, and handle
TEMPORARY_ENTITLEMENT_GRANTseparately fromINITIAL_PURCHASE. - Enforce server-side entitlement checks on every paid API route before you switch the paywall on in production.
Steps 7 and 8 get postponed, and they are the only two that protect revenue rather than enable it. If you are building a subscription product at all, what counts as SaaS is worth a skim, because the billing model shapes the architecture more than the framework does.
Open your codebase and search for your entitlement key. How many distinct files came back, and does your server know about any of them?



