Integrate Stripe subscriptions using Cloud Functions that create Checkout Sessions in subscription mode with your Stripe priceId. A webhook Cloud Function handles lifecycle events: checkout.session.completed creates the subscription record, invoice.paid extends the billing period, and customer.subscription.deleted downgrades the user. The Stripe Customer Portal gives users self-service plan management. Display the current plan, billing history, and an upgrade CTA on the user profile.
Setting Up Recurring Stripe Subscriptions in FlutterFlow
Subscriptions are the most common monetization model for apps. This tutorial connects FlutterFlow to Stripe for recurring billing with full lifecycle management. Users subscribe via Stripe Checkout, webhooks keep your Firestore subscription data in sync, and the Customer Portal handles upgrades, downgrades, and cancellations without you building those UIs.
Prerequisites
- FlutterFlow project with Firebase authentication
- Stripe account with test mode API keys
- Stripe product and price created in the Stripe Dashboard
- Users collection in Firestore with subscription fields
Step-by-step guide
Create subscription fields on the Firestore users collection
Create subscription fields on the Firestore users collection
Add the following fields to your users collection: subscriptionTier (String, default 'free'), stripeCustomerId (String, optional), stripeSubscriptionId (String, optional), currentPeriodEnd (Timestamp, optional). The subscriptionTier field drives feature gating throughout the app. The stripeCustomerId links your user to their Stripe customer object. The currentPeriodEnd tracks when the current billing period expires, useful for showing days remaining and checking if a subscription has lapsed.
Expected result: Users collection has subscription tracking fields ready for Stripe integration.
Build the Cloud Function to create a Stripe Checkout Session
Build the Cloud Function to create a Stripe Checkout Session
Deploy a Cloud Function called createCheckoutSession that receives the userId and priceId. The function checks if the user already has a stripeCustomerId; if not, it creates a new Stripe customer with the user's email. Then it creates a Checkout Session with mode: 'subscription', customer: stripeCustomerId, line_items with the priceId, and success_url and cancel_url pointing back to your app. The function returns the session URL. In FlutterFlow, a Subscribe button calls this Cloud Function via an API call, then launches the returned URL.
1// Cloud Function: createCheckoutSession2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const stripe = require('stripe')(functions.config().stripe.secret);5admin.initializeApp();67exports.createCheckoutSession = functions.https8 .onCall(async (data, context) => {9 const uid = context.auth.uid;10 const priceId = data.priceId;11 const userDoc = await admin.firestore()12 .collection('users').doc(uid).get();13 let customerId = userDoc.data()?.stripeCustomerId;1415 if (!customerId) {16 const customer = await stripe.customers.create({17 email: context.auth.token.email,18 metadata: { firebaseUID: uid },19 });20 customerId = customer.id;21 await admin.firestore().collection('users')22 .doc(uid).update({ stripeCustomerId: customerId });23 }2425 const session = await stripe.checkout.sessions.create({26 mode: 'subscription',27 customer: customerId,28 line_items: [{ price: priceId, quantity: 1 }],29 success_url: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',30 cancel_url: 'https://yourapp.com/pricing',31 });3233 return { url: session.url };34 });Expected result: The Cloud Function creates a Stripe Checkout Session and returns the URL for redirect.
Set up the webhook Cloud Function for subscription lifecycle events
Set up the webhook Cloud Function for subscription lifecycle events
Deploy an HTTP Cloud Function called stripeWebhook that receives Stripe webhook events. Verify the webhook signature using stripe.webhooks.constructEvent with the raw request body and your webhook signing secret. Handle three critical events: checkout.session.completed retrieves the subscription and updates the user document with stripeSubscriptionId, subscriptionTier (based on the priceId to tier mapping), and currentPeriodEnd. invoice.paid extends the currentPeriodEnd to the new period. customer.subscription.deleted resets subscriptionTier to 'free' and clears subscription fields.
1// Cloud Function: stripeWebhook2exports.stripeWebhook = functions.https3 .onRequest(async (req, res) => {4 const sig = req.headers['stripe-signature'];5 const endpointSecret = functions.config().stripe.webhook_secret;6 let event;7 try {8 event = stripe.webhooks.constructEvent(9 req.rawBody, sig, endpointSecret10 );11 } catch (err) {12 return res.status(400).send(`Webhook Error: ${err.message}`);13 }1415 const db = admin.firestore();16 switch (event.type) {17 case 'checkout.session.completed': {18 const session = event.data.object;19 const sub = await stripe.subscriptions20 .retrieve(session.subscription);21 const userSnap = await db.collection('users')22 .where('stripeCustomerId', '==', session.customer)23 .limit(1).get();24 if (!userSnap.empty) {25 await userSnap.docs[0].ref.update({26 stripeSubscriptionId: sub.id,27 subscriptionTier: mapPriceToTier(sub.items.data[0].price.id),28 currentPeriodEnd: admin.firestore.Timestamp29 .fromMillis(sub.current_period_end * 1000),30 });31 }32 break;33 }34 case 'invoice.paid': {35 const invoice = event.data.object;36 const sub = await stripe.subscriptions37 .retrieve(invoice.subscription);38 const userSnap = await db.collection('users')39 .where('stripeCustomerId', '==', invoice.customer)40 .limit(1).get();41 if (!userSnap.empty) {42 await userSnap.docs[0].ref.update({43 currentPeriodEnd: admin.firestore.Timestamp44 .fromMillis(sub.current_period_end * 1000),45 });46 }47 break;48 }49 case 'customer.subscription.deleted': {50 const sub = event.data.object;51 const userSnap = await db.collection('users')52 .where('stripeSubscriptionId', '==', sub.id)53 .limit(1).get();54 if (!userSnap.empty) {55 await userSnap.docs[0].ref.update({56 subscriptionTier: 'free',57 stripeSubscriptionId: null,58 currentPeriodEnd: null,59 });60 }61 break;62 }63 }64 res.status(200).send('OK');65 });Expected result: Webhooks keep the user's subscription tier in sync with Stripe for all lifecycle events.
Add the Stripe Customer Portal for self-service billing
Add the Stripe Customer Portal for self-service billing
Deploy a Cloud Function called createPortalSession that takes the user's stripeCustomerId and creates a Stripe Billing Portal session. The portal lets users upgrade or downgrade plans, update payment methods, view invoices, and cancel subscriptions without you building any of those UIs. In FlutterFlow, add a Manage Subscription button on the profile or settings page. On tap, call the Cloud Function and launch the returned portal URL. Configure the portal in your Stripe Dashboard under Settings > Billing > Customer portal to allow the actions you want.
Expected result: Users can manage their subscription, update payment methods, and cancel via the Stripe Customer Portal.
Display current plan and gate features by subscription tier
Display current plan and gate features by subscription tier
On the profile or settings page, add a Container showing the current plan: read the subscriptionTier field from the user document and display it as a badge (Free, Pro, Business). Show the currentPeriodEnd as 'Renews on {date}' formatted text. For feature gating, use Conditional Visibility throughout your app: check if currentUser.subscriptionTier equals 'pro' or 'business' to show premium features. For pages that require a subscription, add an On Page Load action that checks the tier and shows a paywall overlay or redirects to the pricing page if the user is on the free tier.
Expected result: Users see their current plan details and premium features are gated behind the appropriate subscription tier.
Complete working example
1FIRESTORE SCHEMA:2 users (collection):3 subscriptionTier: String (free|pro|business)4 stripeCustomerId: String (optional)5 stripeSubscriptionId: String (optional)6 currentPeriodEnd: Timestamp (optional)78CLOUD FUNCTION: createCheckoutSession9 Input: priceId10 → Create Stripe customer if needed11 → Create Checkout Session (mode: 'subscription')12 → Return session URL1314CLOUD FUNCTION: stripeWebhook (HTTP)15 Verify webhook signature16 Handle events:17 checkout.session.completed → set tier + subscription fields18 invoice.paid → extend currentPeriodEnd19 customer.subscription.deleted → reset to free tier2021CLOUD FUNCTION: createPortalSession22 Input: stripeCustomerId23 → Create Billing Portal session24 → Return portal URL2526PAGE: PricingPage27 Columns of plan cards (Free, Pro, Business)28 Each with features list + price29 Subscribe button → createCheckoutSession → Launch URL3031PAGE: Settings / Profile32 Container: current plan badge + renewal date33 Button "Manage Subscription" → createPortalSession → Launch URL34 Button "Upgrade" (visible if tier == 'free') → Navigate to Pricing3536FEATURE GATING:37 On Page Load: check subscriptionTier38 If tier < required → show paywall overlay or redirect39 Conditional Visibility on premium features: tier >= 'pro'Common mistakes
Why it's a problem: Not handling the customer.subscription.deleted webhook
How to avoid: Handle the customer.subscription.deleted webhook by resetting the user's subscriptionTier to free and clearing subscription fields in Firestore.
Why it's a problem: Calling the Stripe API directly from the FlutterFlow frontend
How to avoid: Always call Stripe through Cloud Functions. The frontend only receives the Checkout Session URL or Portal URL, never the secret key.
Why it's a problem: Checking subscription status only on login
How to avoid: Check the subscriptionTier and currentPeriodEnd on every protected page load. If currentPeriodEnd is in the past, trigger a re-check with Stripe or downgrade locally.
Best practices
- Handle all subscription lifecycle webhooks: created, renewed, cancelled, and failed payment
- Use the Stripe Customer Portal for billing management instead of building custom UIs
- Check subscription status on every protected page load, not just on login
- Store the stripeCustomerId on the user document to avoid creating duplicate customers
- Use Stripe test mode and test card numbers during development
- Map Stripe priceIds to your app's tier names in a configuration document for easy updates
- Add a failed payment handler that notifies users and provides a grace period
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Set up Stripe subscription payments in a FlutterFlow app. I need Cloud Functions for creating Checkout Sessions (subscription mode), handling webhooks (checkout.session.completed, invoice.paid, customer.subscription.deleted), and generating Customer Portal sessions. Include Firestore schema for tracking subscription tier and billing period. Show how to gate features by tier.
Create a pricing page with three plan cards in a Row (Free, Pro, Business). Each card has a plan name, price, features list, and a Subscribe button. The current plan card has a highlighted border and shows 'Current Plan' instead of the Subscribe button.
Frequently asked questions
How do I test subscriptions without real charges?
Use Stripe test mode with test card number 4242 4242 4242 4242. Stripe test mode creates real subscription objects but never charges real money. You can also use Stripe CLI to trigger test webhook events locally.
Can I offer a free trial before charging?
Yes. Add trial_period_days to the Checkout Session creation: stripe.checkout.sessions.create({ subscription_data: { trial_period_days: 14 } }). The user subscribes but is not charged until the trial ends.
How do I handle failed recurring payments?
Handle the invoice.payment_failed webhook. Update the user document with a paymentFailed flag and show an in-app banner asking them to update their payment method via the Customer Portal. Stripe retries failed payments automatically based on your retry settings.
Can users switch between plans?
Yes. The Stripe Customer Portal handles plan upgrades and downgrades automatically. Configure the available plans in the Stripe Dashboard under Customer Portal settings. Stripe prorates the charge by default.
How do I offer annual vs monthly billing?
Create separate prices in Stripe for the same product: one with interval monthly, another with interval yearly. Show both options on your pricing page and pass the corresponding priceId to the Checkout Session.
Can RapidDev help set up Stripe subscriptions?
Yes. RapidDev can integrate Stripe subscriptions with full webhook handling, feature gating, trial periods, coupon support, and analytics dashboards for your FlutterFlow app.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation