Guide
Flutter subscriptions with Stripe on web and RevenueCat on mobile, on one Firebase backend
On mobile, digital subscriptions generally go through App Store and Google Play billing, while on the web you can charge cards with Stripe. Here is how to run both from one Flutter codebase and check access in a single place, using the architecture Template369 ships.
Updated
The short version
- Mobile: the app buys App Store and Google Play products through the RevenueCat SDK, configured with the Firebase Auth UID as the RevenueCat app user ID.
- Web: the app calls a Cloud Function that creates a Stripe Checkout session and opens the returned URL. The Stripe secret key never leaves the server.
- Access: on every platform, the app reads one Firestore document per user that mirrors RevenueCat's customer record, and walks its entitlements from the highest tier down.
Why two payment systems
On iOS and Android, digital subscriptions sold inside the app generally go through the store's own billing. On the web there is no store, so Stripe is the usual choice. That leaves a Flutter app with two purchase paths and one question that matters everywhere: does this user have Pro right now?
RevenueCat answers that question for both. Its mobile SDK handles store purchases, and RevenueCat can also track Stripe subscriptions and use them to unlock entitlements in your mobile app. The remaining job is getting that answer into your app cheaply on every platform, including Flutter web, where the RevenueCat mobile SDK doesn't run.
1. Model plans once
Each plan carries every identifier it has on every platform: App Store and Google Play product IDs, Stripe price and product IDs, and the RevenueCat entitlement it grants. In Template369 that's a SubscriptionPlanModel per plan in subscription_plans.dart, plus ID lists the app uses to fetch store products:
static List<String> androidPlanIDs = [
AppKeys.starterMonthlyPlan,
AppKeys.proMonthlyPlan,
AppKeys.starterYearlyPlan,
AppKeys.proYearlyPlan,
];Keep a test that every paid plan appears in every list. A plan missing from the Android list simply never shows up on Android, which is easy to miss when you test on iOS. (Template369 shipped exactly that bug until October 2026; the changelog has the fix.)
2. Use the Firebase UID as the RevenueCat app user ID
Configure RevenueCat with the signed-in user's Firebase UID, so RevenueCat's customer and your Firestore user share one ID, and you can store the Stripe customer ID on the same user document:
config = PurchasesConfiguration(AppKeys.revenueCatAppleApiKey)
..appUserID = userId
..store = Store.appStore;
await Purchases.configure(config);Without this, RevenueCat creates anonymous IDs and you end up aliasing users later. With it, a document keyed by UID is all you need to look someone up.
3. Buy on mobile, then read the entitlement you just bought
After Purchases.purchaseStoreProduct returns, look up which entitlement that product grants instead of hard-coding one plan:
final info = await Purchases.purchaseStoreProduct(product);
final entitlementId =
SubscriptionPlans.entitlementForProduct(product.identifier);
final entitlement =
entitlementId == null ? null : info.entitlements.all[entitlementId];
if (entitlement != null && entitlement.isActive) {
// unlock the UI now; the Firestore mirror catches up server-side
}4. Sell on the web through a callable, never from the client
The Flutter web app never sees a Stripe secret. It calls a Cloud Function with the user's Stripe customer ID and the chosen price, and opens the Checkout URL it gets back:
final result = await FirebaseFunctions.instance
.httpsCallable(Constants.createStripeSubscriptionToApp)
.call(CreateStripeSubscriptionToAppFunctionModel(
userId: userId,
userStripeCustomerId: customerId,
stripePriceId: priceId,
).toMap());
if (result.data["statusCode"] == 200) {
launchWebUrl(result.data["data"][Constants.checkoutUrl]);
} Template369 uses seven named callables for the web flow: create a customer, create, update and cancel a subscription, cancel a scheduled change, open the Stripe customer portal, and restore RevenueCat purchases. Every one returns the same {statusCode, message, data} envelope, so the app handles errors in one way. On the server, read the user from the callable's auth context rather than trusting a user_id sent by the client.
5. Check access in one place, on every platform
The app reads revenuecat_customers_collection/{uid}, a Firestore mirror of RevenueCat's customer record, and walks entitlements in priority order so a Pro user is never shown as Starter:
List<String> subscriptionPriority = [
AppKeys.proSubscription,
AppKeys.starterSubscription,
];
for (var key in subscriptionPriority) {
if (!entitlements.containsKey(key)) continue;
final expires = DateTime.parse(entitlements[key]!.expiresDate);
if (expires.isAfter(DateTime.now())) {
// this is the user's plan
break;
}
}The document is written server-side only. RevenueCat's Firebase extension can store customer information in a Firestore collection whose document IDs are app user IDs, or your own webhook handler can write it. Either way, your security rules should let a user read only their own document and never write it.
Because this check is a Firestore read, it works the same on Flutter web, where the RevenueCat mobile SDK isn't available.
Pitfalls worth a test
- An ID missing from one platform's list. Assert every paid plan's IDs are in every list.
- Stripe entitlements identify products, not prices. When RevenueCat reports a Stripe purchase, the identifier you map back to a plan is the Stripe product ID, so keep a product ID list next to the price IDs.
- Secrets in the app bundle. Anything in a Flutter
.envor inlib/ships to users. Stripe secret keys and webhook secrets belong in the backend's secret store. - Trusting the client. Callables get the caller from the auth context; ignore a client-sent user ID.
- Refreshing too late. After a mobile purchase, update the UI from the purchase result straight away rather than waiting for the Firestore mirror.
Doing this with an AI agent
Template369's add-subscription-plan skill walks an agent through every place a plan lives: the store products, RevenueCat entitlements, Stripe prices, the plan model, the ID lists and feature gating, and ends with flutter analyze and flutter test. See Build with an AI agent.