Skip to content
Tech Stack9 August 2026 · 10 min read

Building Subscription-Gated Features in Flutter With RevenueCat in 2026

A Flutter RevenueCat subscription is a store transaction and an entitlement pretending to be one thing. How to gate on entitlements, survive offline, and handle the webhook race.

Building Subscription-Gated Features in Flutter With RevenueCat in 2026

Building Subscription-Gated Features in Flutter With RevenueCat in 2026

The paywall screen is the easy part. The hard part is the single line that asks "is this person allowed?", because by the time you ship it gets called from forty places and every one of them has to agree.

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 seam between them is where the bugs live. My own BookBed property management SaaS bills through Stripe instead, because it started as a web product and owners pay from a browser, so native in-app purchase never entered the critical path. That is exactly why I re-read the docs before writing this rather than trusting my memory of them. The rules move.

Below: what RevenueCat actually replaces, how to gate features so you can change pricing later without a migration, what happens when the network dies mid-flight, and a race condition that is still an open GitHub issue.

What Does RevenueCat Actually Give You Over in_app_purchase?

Three layered sheets of translucent tracing paper on a wooden desk, each inked with part of a wiring diagram, the top sheet curling away from a brass pin above a faint coffee ring

RevenueCat replaces receipt validation, cross-platform subscription state, and the product-to-entitlement mapping that the in_app_purchase plugin leaves entirely to your own backend. Everything else it sells you is analytics and experiments layered on top of that core. The purchases_flutter package is at 10.8.0 as I write this, targeting Xcode 14.0+, iOS 13.0+ and Android SDK 21+.

Concernin_app_purchaseRevenueCatRoll your own
Store transaction plumbingYesYesYou write it
Server-side receipt validationYou build itIncludedYou build it
Product → entitlement mappingNoneDashboard-configuredYour database
Cross-platform subscriber identityNoneappUserIDYour auth system
Subscription state after reinstallManual restore logicCustomerInfo cacheManual
CostFreeFree to $2,500 MTR, then 1%Engineering time

That 1% deserves a second look before you commit. RevenueCat's pricing page is free up to $2,500 in monthly tracked revenue and then charges 1% of what it tracks, and MTR is measured on gross revenue before Apple or Google take their cut. So on a $10 subscription where Apple already kept $3, the 1% is calculated on the ten, not the seven. At small scale that is noise. At $50k MTR it is a salary line item you should have modelled.

One migration hazard worth knowing 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 previously let consumed one-time products be restored. If your non-consumables were misconfigured as consumables in the dashboard, upgrading quietly takes away your customers' ability to restore them. Check that mapping first.

Gate on Entitlements, Never on Product IDs

A single unlabelled brass key on a worn leather fob lit by a window, with a tangled ring of blank-tagged keys pushed back into the shadow behind it

Your app should never know a product identifier exists. It should know the word pro, and nothing else.

This sounds like pedantry until the first time marketing wants a Black Friday price. An entitlement is a named capability in the RevenueCat dashboard; any number of products can grant it. Gate on the entitlement and you can add pro_annual_bf2026, a regional price, a lifetime tier, and a promo code, all without shipping a build. Gate on the product ID and every pricing experiment becomes an App Store review cycle.

Future<bool> hasPro() async {
  final info = await Purchases.getCustomerInfo();
  return info.entitlements.active.containsKey('pro');
}

Actually — that overstates it. There is one place product IDs legitimately belong in client code, and that is the paywall itself, where you render an Offering and need to show prices. Even there, prefer the offering's packages over hardcoded identifiers so the dashboard stays in charge. The rule is narrower than "never": no product ID should ever appear in a conditional that guards a feature.

If you are wiring this into a Firebase backend, the mechanics of syncing entitlement state into custom claims are covered in the Flutter, Firebase and RevenueCat stack guide.

What Happens to a Flutter Subscription When the Device Goes Offline?

A closed grey laptop on an airline tray table with a hard rectangle of window light falling across the lid, a face-down phone and a dried coffee ring beside it

The SDK keeps serving the last cached CustomerInfo, so a paying customer on a plane still passes your entitlement check without a network round trip. That cache is the default behaviour, not a feature you opt into, and it persists until something invalidates it.

The interesting case is the one where RevenueCat's own servers are unreachable. That is what Offline Entitlements handles: the SDK caches the product-to-entitlement relationships from your dashboard and, when a purchase cannot be posted, grants access from the store's own record instead. It switches on automatically when the servers stop responding and switches off again when they recover.

Three limits matter, and the docs are honest about them. The feature is built on StoreKit 2, so it needs iOS 15.0+ or macOS 12.0+. Consumables are excluded, and on Android every in-app purchase still requires server validation. And it needs the app to have launched online at least once, so a fresh install on a plane is not covered.

There is also a webhook you probably have not handled. RevenueCat sends TEMPORARY_ENTITLEMENT_GRANT when it cannot validate a purchase with the store during an outage, granting at most 24 hours of access, per the webhook event types documentation. It later resolves into either an INITIAL_PURCHASE or an EXPIRATION. If your backend treats every grant event as a confirmed sale, you will book revenue that evaporates a day later.

The Webhook Race Nobody Puts in the Quickstart

There is no guarantee that RevenueCat's webhook reaches your server before the client sees the new entitlement. None. The quickstart does not mention it because the quickstart stops at the device.

Issue #1094 on purchases-flutter has been open since 6 June 2024, labelled as an enhancement rather than a bug. The reporter wanted a way to ensure the webhook completes before the app is told about the purchase. There is no such mechanism. The purchase completes on device, CustomerInfo updates locally, your UI opens the gate, and your API may still return 403 for a few hundred milliseconds — or a few seconds if the delivery retries.

So split the trust. Client entitlement state is for rendering: hide the upgrade button and show the pro tab. Server-side entitlement state is for anything that costs you money or leaks data: API quota, file storage, a model call. Never let the client's optimism authorise the server's spend.

On BookBed the same shape shows up with Stripe rather than RevenueCat. Checkout opens in a second tab, the webhook is authoritative, and the original tab has to catch up or the UI lies to the owner for a few seconds after success. Different vendor, identical failure mode. Any time a payment provider talks to two of your systems on different clocks, you need to decide which one is allowed to be wrong.

Restore Purchases Is Not Optional

Apple will reject you without it. That is reason enough, but the better reason is support load: reinstalls and the customer who deleted the app in a bad mood and came back a week later. One button, one call to Purchases.restorePurchases(), placed somewhere a frustrated person can find it in under ten seconds. Not buried in settings.

Family Sharing Will Quietly Flatten Your Revenue Chart

Enable Family Sharing on a subscription and one purchase can serve up to six people. Your entitlement logic handles this correctly with no changes, because RevenueCat surfaces the shared access as a normal active entitlement.

Your dashboard does not behave the way you expect, though. RevenueCat's Apple Family Sharing documentation notes that family-shared transactions are excluded from charts data and ignored in most overview metrics apart from installs and active users. Webhook payloads carry an is_family_share boolean, which is App Store only and always false on other stores. If active users climb while revenue looks flat, check that flag before rewriting your onboarding.

How Should You Design the Paywall Screen Itself?

Show one screen after onboarding with a free-versus-paid comparison, a single highlighted plan, and a call to action naming the outcome rather than the transaction. That is the whole formula, and most of the variance comes from where you place it, not how it looks.

The strategic decision sits above the design. RevenueCat's State of Subscription Apps 2026 report, drawn from more than 115,000 apps and over $16 billion in tracked revenue, found hard paywalls converting at a median 10.7% day-35 trial-to-paid against 2.1% for freemium. That is a roughly fivefold gap, and it is the single largest lever on this page. It is also a lever with a cost, since a hard paywall trades install volume for conversion rate, and the report notes hard-paywall conversion itself slipped from 12.1% the previous year.

You have probably felt the wrong version of this as a user. App opens. Full-screen offer before you have seen a single thing the app does. Close. That is a hard paywall placed before value rather than after it, and it converts the small number of people who already trusted the App Store listing.

For the tooling side of building and testing these screens, the Flutter developer tools I keep installed covers the debugging setup. The eight FlutterFlow marketplace templates I maintain taught me that paywall placement gets edited more often than paywall styling, which is a decent argument for keeping the layout dumb and the trigger configurable.

The Order I Would Ship It In

  1. Configure products in App Store Connect and Google Play Console first, and get them into a reviewable state. Everything downstream blocks on this and it is the slowest step.
  2. Create entitlements in the RevenueCat dashboard, then attach products to them. Name entitlements after capabilities (pro, team_seats), never after price points.
  3. Add purchases_flutter and call Purchases.configure once at startup, before any UI that reads entitlement state.
  4. Log in your own user identifier with Purchases.logIn so the subscriber survives reinstalls and follows the account across devices.
  5. Write a single hasEntitlement(String key) helper and route every gate through it. One function, one place to add logging later.
  6. Add restore purchases to settings and test it on a device that has never seen the app.
  7. Point the webhook at your backend, verify the authorization header on every delivery, and handle TEMPORARY_ENTITLEMENT_GRANT distinctly from INITIAL_PURCHASE.
  8. Enforce server-side entitlement checks on every paid API route before you enable the paywall in production.

Steps 7 and 8 are the ones that get postponed and then forgotten. They are also the only two on the list that protect revenue rather than enable it. If you are building a subscription product at all, the definitions in what counts as SaaS are worth a skim, because the billing model you pick shapes the architecture more than the framework does.

Open your app and search the codebase for your entitlement key. How many distinct files came back, and does your server know about any of them?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

How do you check RevenueCat entitlements in a Flutter app?

Call Purchases.getCustomerInfo() and test whether your entitlement key appears in the entitlements.active map it returns. That single check is the whole gate, and it should live in one helper function that every feature calls rather than being copy-pasted into widgets. The call is cheap because the SDK caches the latest CustomerInfo and refreshes it when the app becomes active, so it usually resolves without a network round trip. Check the entitlement key, never the product identifier. Entitlements are configured in the RevenueCat dashboard and one entitlement can be granted by any number of products, which is what lets you change pricing without shipping a build.

Should you use RevenueCat or the in_app_purchase plugin?

Use the in_app_purchase plugin if you already run a backend that can validate App Store and Play receipts, and use RevenueCat if you would rather not build and maintain that layer. Both wrap StoreKit and Google Play Billing, so the store plumbing is comparable. The difference is everything after the transaction: server-side validation, cross-platform subscriber identity, entitlement mapping, and state that survives a reinstall. RevenueCat's own pricing page puts it at free up to $2,500 in monthly tracked revenue and then 1%, measured on gross revenue before Apple or Google take their commission. Model that 1% against your own engineering hours before deciding.

How do you design a Flutter paywall that converts?

Show one paywall right after onboarding, put free and paid side by side on a single screen, highlight one plan, and name the outcome in the button rather than the transaction. Placement matters more than styling. RevenueCat's State of Subscription Apps 2026 report, covering over 115,000 apps, found hard paywalls converting at a median 10.7% day-35 trial-to-paid against 2.1% for freemium. A hard paywall also trades install volume for that rate, and the same report shows hard-paywall conversion fell from 12.1% the year before. Pick the trade deliberately instead of copying whichever app you last downloaded.

Do RevenueCat entitlements work offline in Flutter?

Yes. The SDK serves the last cached CustomerInfo, so an existing subscriber keeps access on a plane or a dead network without any extra code from you. There is a second, separate mechanism for when RevenueCat's own servers are unreachable: the Offline Entitlements feature grants access from the store's own record using cached product-to-entitlement relationships, switching on and off automatically. Its limits are specific. It relies on StoreKit 2, so it needs iOS 15.0 or macOS 12.0 and later, it excludes consumables, every Android in-app purchase still requires server validation, and the app must have launched online at least once beforehand.

What breaks if you gate features on product IDs instead of entitlements?

Every pricing change turns into an App Store review cycle, because the identifiers your conditionals check are compiled into the binary. Add a Black Friday price, a regional tier, a lifetime option, or a promo product and each one needs a new build before existing users can benefit from it. Gate on an entitlement instead and all of that happens in the RevenueCat dashboard, where you attach new products to the same named capability. The one legitimate place for product identifiers in client code is the paywall itself, where you render prices, and even there you should read them from the offering's packages rather than hardcoding.