Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Set Up a Content Subscription or Membership Site in FlutterFlow

Create a membership site by adding a subscriptionTier field to your users collection, gating content pages with an On Page Load tier check that shows a paywall overlay for unauthorized users, processing payments through a Stripe Checkout subscription Cloud Function, and automatically updating tiers via a Stripe webhook. The content collection uses a requiredTier field so Backend Queries only return items the user can access.

What you'll learn

  • How to model subscription tiers on user documents and gate content with requiredTier fields
  • How to build a paywall overlay with blur effect and upgrade call-to-action
  • How to trigger Stripe Checkout in subscription mode via a Cloud Function
  • How to handle Stripe webhooks to automatically update user tier and expiry
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read25-30 minFlutterFlow Pro+ (Cloud Functions required)March 2026RapidDev Engineering Team
TL;DR

Create a membership site by adding a subscriptionTier field to your users collection, gating content pages with an On Page Load tier check that shows a paywall overlay for unauthorized users, processing payments through a Stripe Checkout subscription Cloud Function, and automatically updating tiers via a Stripe webhook. The content collection uses a requiredTier field so Backend Queries only return items the user can access.

Building a Content Subscription System in FlutterFlow

Membership sites let you monetize exclusive content by restricting access to paying subscribers. This tutorial walks through the full system: tier-based user fields, a visually appealing paywall overlay, Stripe subscription payments via Cloud Functions, webhook-driven tier management, and a filtered content library that only shows items the user is entitled to view.

Prerequisites

  • A FlutterFlow project on the Pro plan or higher
  • Firebase project with Firestore and Authentication enabled
  • A Stripe account with at least one subscription Price created in the Stripe Dashboard
  • Basic familiarity with Firestore collections and FlutterFlow Backend Queries

Step-by-step guide

1

Add subscription fields to the users collection

Open the Firestore schema in FlutterFlow and add three fields to your users document: subscriptionTier (String, default 'free'), subscriptionExpiresAt (Timestamp, nullable), and stripeCustomerId (String, nullable). The subscriptionTier field will hold values like 'free', 'basic', or 'premium'. Create a content collection with fields: title (String), body (String), thumbnailUrl (String), requiredTier (String), and createdAt (Timestamp). This separation lets you gate individual content items by tier.

Expected result: Your users collection has subscriptionTier, subscriptionExpiresAt, and stripeCustomerId fields. The content collection has a requiredTier field on every document.

2

Build the content library page with tier-filtered Backend Query

Create a new page called ContentLibrary. Add a Backend Query on a ListView that queries the content collection. Add a filter where requiredTier is less than or equal to the current user's subscriptionTier using a custom ordering (free=0, basic=1, premium=2 via a Custom Function that maps tier strings to integers). For a simpler approach, query all content and use Conditional Visibility on each list item to show a lock icon overlay on items above the user's tier. Display each item as a Container with the thumbnail, title, and a lock icon Stack overlay for gated items.

Expected result: The content library displays all items with unlocked content fully visible and locked content showing a lock icon overlay on the thumbnail.

3

Create the paywall overlay with blur and upgrade CTA

On the content detail page, wrap the main content Column in a Stack. Add a second child to the Stack: a Container with a BackdropFilter (Custom Widget using ImageFilter.blur with sigmaX and sigmaY set to 8) that covers the full page. Inside this blurred overlay, add a Column with a lock Icon, a Text widget reading 'Premium Content', a description of what the tier includes, and a Button labeled 'Upgrade Now'. Set Conditional Visibility on the blur Container to show only when currentUserDocument.subscriptionTier is less than the content's requiredTier.

Expected result: Free users who navigate to a premium content page see a blurred overlay with an Upgrade Now button. Subscribed users see the full content without any overlay.

4

Set up the Stripe Checkout subscription Cloud Function

Create a Cloud Function called createSubscriptionCheckout. It receives the userId and priceId as parameters. The function looks up or creates a Stripe Customer (storing stripeCustomerId on the user doc), then calls stripe.checkout.sessions.create with mode set to 'subscription', the priceId, success and cancel URLs pointing back to your app, and customer set to the Stripe customer ID. Return the session URL. In FlutterFlow, wire the Upgrade Now button to call this Cloud Function via an API Call action, then use a Launch URL action with the returned session URL to open the Stripe Checkout page.

createSubscriptionCheckout.js
1// Cloud Function: createSubscriptionCheckout
2const functions = require('firebase-functions');
3const stripe = require('stripe')(functions.config().stripe.secret_key);
4const admin = require('firebase-admin');
5admin.initializeApp();
6
7exports.createSubscriptionCheckout = functions.https.onCall(async (data, context) => {
8 const { priceId } = data;
9 const uid = context.auth.uid;
10 const userDoc = await admin.firestore().collection('users').doc(uid).get();
11 let customerId = userDoc.data().stripeCustomerId;
12
13 if (!customerId) {
14 const customer = await stripe.customers.create({
15 email: userDoc.data().email,
16 metadata: { firebaseUID: uid },
17 });
18 customerId = customer.id;
19 await admin.firestore().collection('users').doc(uid).update({ stripeCustomerId: customerId });
20 }
21
22 const session = await stripe.checkout.sessions.create({
23 mode: 'subscription',
24 customer: customerId,
25 line_items: [{ price: priceId, quantity: 1 }],
26 success_url: 'https://yourapp.com/subscription-success',
27 cancel_url: 'https://yourapp.com/pricing',
28 });
29
30 return { url: session.url };
31});

Expected result: Tapping Upgrade Now opens Stripe Checkout in the browser where the user completes subscription payment.

5

Handle Stripe webhook to update user tier automatically

Create a second Cloud Function called stripeWebhook that listens for Stripe webhook events. Verify the webhook signature using stripe.webhooks.constructEvent. Handle the checkout.session.completed event: extract the customer ID, look up the user by stripeCustomerId, determine the tier from the priceId metadata, and update the user's subscriptionTier and subscriptionExpiresAt fields. Also handle customer.subscription.deleted to revert the user to the free tier. Register this webhook URL in your Stripe Dashboard under Developers > Webhooks.

stripeWebhook.js
1// Cloud Function: stripeWebhook
2exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
3 const sig = req.headers['stripe-signature'];
4 let event;
5 try {
6 event = stripe.webhooks.constructEvent(req.rawBody, sig, functions.config().stripe.webhook_secret);
7 } catch (err) {
8 return res.status(400).send(`Webhook Error: ${err.message}`);
9 }
10
11 if (event.type === 'checkout.session.completed') {
12 const session = event.data.object;
13 const subscription = await stripe.subscriptions.retrieve(session.subscription);
14 const priceId = subscription.items.data[0].price.id;
15 const tier = priceId === 'price_premium_id' ? 'premium' : 'basic';
16 const usersRef = admin.firestore().collection('users');
17 const snapshot = await usersRef.where('stripeCustomerId', '==', session.customer).get();
18 if (!snapshot.empty) {
19 await snapshot.docs[0].ref.update({
20 subscriptionTier: tier,
21 subscriptionExpiresAt: admin.firestore.Timestamp.fromMillis(subscription.current_period_end * 1000),
22 });
23 }
24 }
25
26 if (event.type === 'customer.subscription.deleted') {
27 const subscription = event.data.object;
28 const usersRef = admin.firestore().collection('users');
29 const snapshot = await usersRef.where('stripeCustomerId', '==', subscription.customer).get();
30 if (!snapshot.empty) {
31 await snapshot.docs[0].ref.update({ subscriptionTier: 'free', subscriptionExpiresAt: null });
32 }
33 }
34
35 res.status(200).send('OK');
36});

Expected result: After a successful Stripe payment, the user's subscriptionTier field updates automatically. Cancellations revert the user to the free tier.

6

Add expiry checking and Stripe Customer Portal link

On every protected page, add an On Page Load action that checks whether currentUserDocument.subscriptionExpiresAt is before the current timestamp. If expired, update subscriptionTier to 'free' via an Update Document action and show the paywall. For managing subscriptions, add a Manage Subscription button on the settings page. Create a Cloud Function called createPortalSession that calls stripe.billingPortal.sessions.create with the user's stripeCustomerId and returns the portal URL. Wire the button to call this function and open the returned URL, allowing users to cancel or change plans through Stripe's hosted portal.

Expected result: Expired subscriptions are caught on each page load. Users can manage, upgrade, or cancel their subscription through the Stripe Customer Portal.

Complete working example

createSubscriptionCheckout.js
1// Cloud Function: Full Subscription Management
2// Includes checkout creation, webhook handling, and portal session
3
4const functions = require('firebase-functions');
5const stripe = require('stripe')(functions.config().stripe.secret_key);
6const admin = require('firebase-admin');
7admin.initializeApp();
8
9// Create Stripe Checkout for subscription
10exports.createSubscriptionCheckout = functions.https.onCall(async (data, context) => {
11 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
12 const { priceId } = data;
13 const uid = context.auth.uid;
14 const userDoc = await admin.firestore().collection('users').doc(uid).get();
15 let customerId = userDoc.data().stripeCustomerId;
16
17 if (!customerId) {
18 const customer = await stripe.customers.create({
19 email: userDoc.data().email,
20 metadata: { firebaseUID: uid },
21 });
22 customerId = customer.id;
23 await admin.firestore().collection('users').doc(uid).update({ stripeCustomerId: customerId });
24 }
25
26 const session = await stripe.checkout.sessions.create({
27 mode: 'subscription',
28 customer: customerId,
29 line_items: [{ price: priceId, quantity: 1 }],
30 success_url: 'https://yourapp.com/subscription-success',
31 cancel_url: 'https://yourapp.com/pricing',
32 });
33 return { url: session.url };
34});
35
36// Stripe Webhook Handler
37exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
38 const sig = req.headers['stripe-signature'];
39 let event;
40 try {
41 event = stripe.webhooks.constructEvent(
42 req.rawBody, sig, functions.config().stripe.webhook_secret
43 );
44 } catch (err) {
45 return res.status(400).send(`Webhook Error: ${err.message}`);
46 }
47
48 const usersRef = admin.firestore().collection('users');
49
50 if (event.type === 'checkout.session.completed') {
51 const session = event.data.object;
52 const subscription = await stripe.subscriptions.retrieve(session.subscription);
53 const priceId = subscription.items.data[0].price.id;
54 const tier = priceId === 'price_premium_id' ? 'premium' : 'basic';
55 const snapshot = await usersRef.where('stripeCustomerId', '==', session.customer).get();
56 if (!snapshot.empty) {
57 await snapshot.docs[0].ref.update({
58 subscriptionTier: tier,
59 subscriptionExpiresAt: admin.firestore.Timestamp.fromMillis(
60 subscription.current_period_end * 1000
61 ),
62 });
63 }
64 }
65
66 if (event.type === 'customer.subscription.deleted') {
67 const sub = event.data.object;
68 const snapshot = await usersRef.where('stripeCustomerId', '==', sub.customer).get();
69 if (!snapshot.empty) {
70 await snapshot.docs[0].ref.update({ subscriptionTier: 'free', subscriptionExpiresAt: null });
71 }
72 }
73 res.status(200).send('OK');
74});
75
76// Customer Portal Session
77exports.createPortalSession = functions.https.onCall(async (data, context) => {
78 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
79 const uid = context.auth.uid;
80 const userDoc = await admin.firestore().collection('users').doc(uid).get();
81 const customerId = userDoc.data().stripeCustomerId;
82 const session = await stripe.billingPortal.sessions.create({
83 customer: customerId,
84 return_url: 'https://yourapp.com/settings',
85 });
86 return { url: session.url };
87});

Common mistakes

Why it's a problem: Checking subscription status only on login instead of every protected page load

How to avoid: Add an On Page Load action on every protected page that compares subscriptionExpiresAt against the current timestamp and downgrades expired users immediately.

Why it's a problem: Gating content only in the UI without Firestore Security Rules

How to avoid: Add Firestore Security Rules that check the requesting user's subscriptionTier against the content document's requiredTier before allowing reads.

Why it's a problem: Crediting the subscription tier optimistically before Stripe confirms payment

How to avoid: Only update subscriptionTier inside the Stripe webhook handler after receiving a confirmed checkout.session.completed event.

Why it's a problem: Hardcoding Stripe price IDs in FlutterFlow instead of storing them in Firestore

How to avoid: Store price IDs in a Firestore pricing_tiers collection and read them dynamically so tier changes take effect without an app update.

Best practices

  • Store Stripe API keys in Cloud Function environment config, never in client-side code
  • Use Stripe's hosted Customer Portal for subscription management instead of building your own cancellation flow
  • Add a subscriptionExpiresAt field and check it on every protected page load for real-time enforcement
  • Show a content teaser (first paragraph or blurred preview) before the paywall to motivate upgrades
  • Log all tier changes in a subscription_events collection for auditing and debugging
  • Use Stripe's test mode with test card 4242 4242 4242 4242 to verify the full flow before going live
  • Implement graceful degradation: if the tier check fails due to a network error, default to the cached tier rather than locking the user out

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to build a membership site in FlutterFlow with Stripe subscriptions. Show me the Firestore schema for users with subscription tiers, a content collection with tier gating, the Cloud Function for creating a Stripe Checkout subscription session, and the webhook handler that updates the user's tier on payment success.

FlutterFlow Prompt

Create a content library page where each item shows a lock icon if the user's subscription tier is below the content's required tier. Add a paywall overlay with a blurred background and an Upgrade Now button that opens Stripe Checkout.

Frequently asked questions

Can I offer both monthly and annual subscription options?

Yes. Create separate Price objects in Stripe (one monthly, one annual) and display both options on your pricing page. Pass the selected priceId to the createSubscriptionCheckout Cloud Function.

How do I handle free trial periods before charging?

Add trial_period_days to your Stripe Checkout session creation. Set the user's tier to the trial tier immediately and let the webhook handle conversion or expiry when the trial ends.

What happens if the Stripe webhook fails to update the user document?

Stripe retries failed webhooks for up to 3 days. Add error logging in your Cloud Function and consider a scheduled function that reconciles Stripe subscription statuses with your Firestore user documents daily.

Can I gate specific features instead of entire content pages?

Yes. Use Conditional Visibility on any widget, checking currentUserDocument.subscriptionTier. You can gate buttons, sections, or entire pages using the same tier comparison logic.

How do I prevent users from sharing their account to bypass the paywall?

Track active sessions in Firestore and limit concurrent logins. On each login, check if another session exists and force-logout the previous one, similar to how streaming services handle single-screen plans.

Can RapidDev help build a production membership platform?

Yes. RapidDev can implement the full subscription system including multi-tier pricing, Stripe integration, webhook handling, content gating, and analytics dashboards for subscriber metrics.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.