Create a subscription paywall by storing a subscriptionTier field on each user document. Display premium content with a lock overlay and blurred preview for non-subscribers. Tapping locked content opens a Bottom Sheet with pricing tiers. The Subscribe button redirects to Stripe Checkout, and a Cloud Function webhook updates the user tier on successful payment. Conditional Visibility gates all premium content on both the UI and Firestore Security Rules level.
Building a Content Subscription Paywall in FlutterFlow
Subscription models let you monetize content by offering free teasers and locking full access behind a paywall. This tutorial walks through creating a complete subscription system in FlutterFlow with Stripe payments, content gating at both the UI and database level, and a polished paywall experience for non-subscribers.
Prerequisites
- A FlutterFlow project with Firestore and Firebase Authentication configured
- A Stripe account with API keys stored in Cloud Function environment variables
- A content collection in Firestore with at least a few test documents
- Basic familiarity with FlutterFlow Action Flows
Step-by-step guide
Set up the Firestore data model for subscriptions and content
Set up the Firestore data model for subscriptions and content
Add a subscriptionTier field (String: 'free', 'monthly', 'annual') and subscriptionExpiry (Timestamp) to your users collection. In your content collection, add an isPremium boolean field. Create a subscription_plans collection with documents for each tier containing: name, priceMonthly, priceAnnual, features (String Array), and stripePriceId. This structure lets you manage plans from Firestore without redeploying the app.
Expected result: Firestore has users with subscriptionTier, content with isPremium, and subscription_plans with Stripe Price IDs.
Build the premium content card with lock overlay
Build the premium content card with lock overlay
Create a ContentCard Component with parameters: title, thumbnailUrl, isPremium, isSubscribed. Use a Stack widget. The bottom layer shows the content thumbnail and title. Add a second layer with Conditional Visibility set to isPremium == true AND isSubscribed == false. This overlay layer is a Container with a semi-transparent black background, a lock Icon centered, and a gradient fade from transparent to dark at the bottom. Below the Stack, show a teaser Text (first 100 characters of content) with a 'Subscribe to continue reading' Text below it, both conditionally visible for non-subscribers.
Expected result: Premium content shows a lock overlay and blurred teaser for non-subscribers, while subscribers see the full content.
Create the paywall Bottom Sheet with pricing tiers
Create the paywall Bottom Sheet with pricing tiers
Create a PaywallSheet Component. Use a Column with: a headline Text 'Unlock Premium Content', a subtitle Text with the value proposition, then a Row of two Container cards side by side. The left card shows Monthly pricing and the right shows Annual pricing with a 'Save 40%' badge Container positioned in the top-right corner. Each card has: tier name Text, price Text (large headlineMedium), features ListView from subscription_plans query, and a Subscribe Button. Use a Page State variable selectedPlan to track which tier is tapped. Highlight the selected card with a Primary border.
Expected result: A polished pricing Bottom Sheet appears with monthly and annual options, feature lists, and subscribe buttons.
Connect Stripe Checkout via Cloud Function
Connect Stripe Checkout via Cloud Function
Create a Cloud Function createCheckoutSession that receives userId and stripePriceId, creates a Stripe Checkout Session in subscription mode with success and cancel URLs, and returns the session URL. In FlutterFlow, on the Subscribe Button tap: call the Cloud Function via API Call with the selected plan stripePriceId, then use Launch URL action to open the returned Checkout URL. Create a second Cloud Function as a Stripe webhook listener for checkout.session.completed that reads the customer email, finds the user document, and updates subscriptionTier and subscriptionExpiry.
1// Cloud Function: createCheckoutSession2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34exports.createCheckoutSession = async (req, res) => {5 const { userId, priceId } = req.body;6 const session = await stripe.checkout.sessions.create({7 mode: 'subscription',8 payment_method_types: ['card'],9 line_items: [{ price: priceId, quantity: 1 }],10 success_url: 'https://yourapp.com/success',11 cancel_url: 'https://yourapp.com/cancel',12 metadata: { userId },13 });14 res.json({ url: session.url });15};Expected result: Tapping Subscribe opens Stripe Checkout. After payment, the webhook updates the user subscriptionTier in Firestore.
Gate content with Conditional Visibility and Firestore Security Rules
Gate content with Conditional Visibility and Firestore Security Rules
On every page or Component that displays premium content, wrap the full content in a Container with Conditional Visibility: isPremium == false OR currentUserDocument.subscriptionTier != 'free'. This ensures non-subscribers only see the teaser. Critically, also add Firestore Security Rules that block reads on premium content fields for users without a valid subscription. In rules, check get(/databases/$(database)/documents/users/$(request.auth.uid)).data.subscriptionTier != 'free' before allowing reads on premium documents. UI gating alone is not secure because users can query Firestore directly.
Expected result: Premium content is gated at both the FlutterFlow UI level and the Firestore Security Rules level.
Add a subscription badge and manage renewals
Add a subscription badge and manage renewals
On the user profile page, add a Container badge showing the current subscription tier with a colored background (gold for annual, blue for monthly, grey for free). Display subscriptionExpiry as 'Renews on [date]' Text below the badge. Add a Manage Subscription Button that opens the Stripe Customer Portal via a Cloud Function returning the portal URL. For expiry handling, add an On Page Load action that checks if subscriptionExpiry is in the past and subscriptionTier is not free, then calls a Cloud Function to verify the subscription status with Stripe and updates the tier if expired.
Expected result: Users see their subscription badge on their profile and can manage billing through the Stripe Customer Portal.
Complete working example
1FIRESTORE DATA MODEL:2 users/{uid}3 subscriptionTier: "free" | "monthly" | "annual"4 subscriptionExpiry: Timestamp5 stripeCustomerId: String67 content/{contentId}8 title: String9 body: String10 thumbnailUrl: String11 isPremium: Boolean12 category: String1314 subscription_plans/{planId}15 name: "Monthly" | "Annual"16 priceMonthly: Number17 priceAnnual: Number18 features: [String]19 stripePriceId: String2021CONTENT CARD COMPONENT:22 Stack23 ├── Column24 │ ├── Image (thumbnailUrl)25 │ └── Text (title)26 └── Container (lock overlay)27 Conditional Visibility: isPremium && !isSubscribed28 Background: semi-transparent black29 ├── Icon (lock, white, size 40)30 └── Text ("Subscribe to unlock")3132PAYWALL BOTTOM SHEET:33 Column34 ├── Text ("Unlock Premium Content", headlineMedium)35 ├── Text ("Get unlimited access to all content")36 ├── Row37 │ ├── Container (Monthly card)38 │ │ ├── Text ("Monthly")39 │ │ ├── Text ("$9.99/mo", headlineLarge)40 │ │ ├── ListView (features)41 │ │ └── Button ("Subscribe Monthly")42 │ └── Container (Annual card + Save badge)43 │ ├── Text ("Annual")44 │ ├── Text ("$5.99/mo", headlineLarge)45 │ ├── ListView (features)46 │ └── Button ("Subscribe Annual")47 └── Text ("Cancel anytime", caption)4849ACTION FLOW — Subscribe Button:50 1. API Call: createCheckoutSession(userId, stripePriceId)51 2. Launch URL: response.url5253WEBHOOK — checkout.session.completed:54 1. Read metadata.userId55 2. Update users/{userId}: subscriptionTier, subscriptionExpiry5657FIRESTORE SECURITY RULES:58 match /content/{doc} {59 allow read: if !resource.data.isPremium60 || get(/users/$(request.auth.uid)).data.subscriptionTier != 'free';61 }Common mistakes
Why it's a problem: Only hiding premium content in the UI without Firestore Security Rules
How to avoid: Add Firestore Security Rules that check the user subscriptionTier before allowing reads on premium content documents.
Why it's a problem: Crediting the subscription tier on the success redirect URL instead of the webhook
How to avoid: Always update the subscriptionTier in the Stripe webhook handler (checkout.session.completed), which is guaranteed to fire on successful payment.
Why it's a problem: Hardcoding Stripe Price IDs in the FlutterFlow app
How to avoid: Store Stripe Price IDs in the subscription_plans Firestore collection. The app reads them dynamically, so you can update pricing without redeploying.
Best practices
- Gate premium content at both the UI (Conditional Visibility) and database (Firestore Security Rules) levels
- Use Stripe webhooks for subscription status updates, never client-side success redirects
- Store Stripe Price IDs in Firestore for dynamic pricing updates without app redeployment
- Show a compelling teaser (first paragraph + blurred remainder) to convert free users
- Display subscription expiry and renewal dates clearly on the user profile
- Provide a Stripe Customer Portal link for users to manage their own billing
- Check subscription expiry on app load to handle lapsed subscriptions gracefully
- Include a free trial option in your Stripe Checkout configuration to reduce conversion friction
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to build a subscription paywall in FlutterFlow with Stripe. Show me the Firestore data model for users with subscription tiers, premium content gating with lock overlays, a pricing Bottom Sheet, Stripe Checkout integration via Cloud Function, and Firestore Security Rules that block premium content for non-subscribers.
Create a content feed page where some items have a lock icon overlay. Add a bottom sheet with two pricing cards side by side for monthly and annual subscriptions, each with a subscribe button.
Frequently asked questions
Can I offer a free trial before charging for the subscription?
Yes. When creating the Stripe Checkout Session, add subscription_data.trial_period_days (e.g., 7 or 14). The user enters payment info but is not charged until the trial ends. Update subscriptionTier immediately so they get access during the trial.
How do I handle subscription cancellations?
Listen for the customer.subscription.deleted Stripe webhook event. When received, update the user subscriptionTier to 'free' and clear subscriptionExpiry. Optionally show a 'Your subscription has ended' banner with a resubscribe button.
Can I have more than two subscription tiers?
Yes. Add more documents to subscription_plans in Firestore and create corresponding Stripe Price objects. Adjust your paywall Bottom Sheet layout to accommodate additional plan cards, using a horizontal ListView if you have more than three tiers.
How do I prevent content sharing between subscribers and non-subscribers?
Firestore Security Rules are your enforcement layer. Even if a subscriber shares a direct link, the non-subscriber cannot load the full content because the Security Rules check their subscriptionTier before allowing the read.
Does this work with Apple and Google in-app purchases instead of Stripe?
FlutterFlow supports RevenueCat integration for in-app purchases on iOS and Android. The content gating logic remains the same; only the payment flow changes from Stripe Checkout to the native purchase dialog.
Can RapidDev help implement a full subscription system?
Yes. RapidDev can build the complete subscription flow including Stripe integration, webhook handling, content gating, free trials, upgrade/downgrade logic, and Apple/Google in-app purchase support.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation