# How to Set Up a Subscription Model for Premium Content in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 25-35 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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.

### 3. 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.

### 4. 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.

```
// Cloud Function: createCheckoutSession
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

exports.createCheckoutSession = async (req, res) => {
  const { userId, priceId } = req.body;
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: 'https://yourapp.com/success',
    cancel_url: 'https://yourapp.com/cancel',
    metadata: { userId },
  });
  res.json({ url: session.url });
};
```

**Expected result:** Tapping Subscribe opens Stripe Checkout. After payment, the webhook updates the user subscriptionTier in Firestore.

### 5. 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.

### 6. 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 code example

File: `FlutterFlow Subscription Setup`

```text
FIRESTORE DATA MODEL:
  users/{uid}
    subscriptionTier: "free" | "monthly" | "annual"
    subscriptionExpiry: Timestamp
    stripeCustomerId: String

  content/{contentId}
    title: String
    body: String
    thumbnailUrl: String
    isPremium: Boolean
    category: String

  subscription_plans/{planId}
    name: "Monthly" | "Annual"
    priceMonthly: Number
    priceAnnual: Number
    features: [String]
    stripePriceId: String

CONTENT CARD COMPONENT:
  Stack
    ├── Column
    │     ├── Image (thumbnailUrl)
    │     └── Text (title)
    └── Container (lock overlay)
          Conditional Visibility: isPremium && !isSubscribed
          Background: semi-transparent black
          ├── Icon (lock, white, size 40)
          └── Text ("Subscribe to unlock")

PAYWALL BOTTOM SHEET:
  Column
    ├── Text ("Unlock Premium Content", headlineMedium)
    ├── Text ("Get unlimited access to all content")
    ├── Row
    │     ├── Container (Monthly card)
    │     │     ├── Text ("Monthly")
    │     │     ├── Text ("$9.99/mo", headlineLarge)
    │     │     ├── ListView (features)
    │     │     └── Button ("Subscribe Monthly")
    │     └── Container (Annual card + Save badge)
    │           ├── Text ("Annual")
    │           ├── Text ("$5.99/mo", headlineLarge)
    │           ├── ListView (features)
    │           └── Button ("Subscribe Annual")
    └── Text ("Cancel anytime", caption)

ACTION FLOW — Subscribe Button:
  1. API Call: createCheckoutSession(userId, stripePriceId)
  2. Launch URL: response.url

WEBHOOK — checkout.session.completed:
  1. Read metadata.userId
  2. Update users/{userId}: subscriptionTier, subscriptionExpiry

FIRESTORE SECURITY RULES:
  match /content/{doc} {
    allow read: if !resource.data.isPremium
      || get(/users/$(request.auth.uid)).data.subscriptionTier != 'free';
  }
```

## Common mistakes

- **Only hiding premium content in the UI without Firestore Security Rules** — Users can bypass FlutterFlow and query Firestore directly via the API, reading all premium content without a subscription. Fix: Add Firestore Security Rules that check the user subscriptionTier before allowing reads on premium content documents.
- **Crediting the subscription tier on the success redirect URL instead of the webhook** — The user might close the browser before the redirect completes, or the redirect might fail. The subscription would never be activated despite payment. Fix: Always update the subscriptionTier in the Stripe webhook handler (checkout.session.completed), which is guaranteed to fire on successful payment.
- **Hardcoding Stripe Price IDs in the FlutterFlow app** — Changing prices requires a new app build and deployment. Different environments (test vs live) need different Price IDs. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-set-up-a-subscription-model-for-premium-content-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-set-up-a-subscription-model-for-premium-content-in-flutterflow
