Intro
Adding a subscription to your web application is probably the easiest way to generate a MRR with your new shiny startup idea. And Firebase is probably the fastest way of integrating a lot of functionality like Authentication, a NoSQL database and Cloud Functions into your system. I have done this a couple of times and just did it again for https://shotmetrics-ai.com, I can recommend Stripe. Integrating Stripe with Firebase means that we will create several Cloud Functions to provide all functionality, including a webhook for accepting incoming Stripe data and using these functions in your frontend.
Let's go through all steps required to implement stripe subscriptions in Firebase applications, using Firebase Firestore, Firebase Cloud Functions and of course Stripe it self.
Step-by-Step Guide
Step 1: Set Up Your Stripe Account
Before we dive into the necessary code, ensure you have a properly configured Stripe account:
- Create a Stripe account if you don't have one already
- Add your products in the Stripe dashboard
- Create pricing plans for all products in Stripe's test mode
- Write down your product and price IDs for later use
Remember to use Stripe's test mode during development so that you don't have to pay for your own services.
Step 2: Create a Cloud Function that returns Stripe Products and Prices
The first function we are going to create is a centralized information endpoint about your subscriptions, so that you don't have to redefine this in the frontend too.
Create a Cloud Function that serves as a central repository for your product and pricing information and add all your products and their prices:
export const getSubscriptionPlans = {
basic: {
id: "basic",
name: "Basic Plan",
description: "Access to core features",
prices: {
monthly: {
id: "price_monthly_basic_id_from_stripe",
amount: 9.99,
interval: "month",
},
yearly: {
id: "price_yearly_basic_id_from_stripe",
amount: 99.99,
interval: "year",
},
},
},
pro: {
id: "pro",
name: "Pro Plan",
description: "Full access to all features",
prices: {
monthly: {
id: "price_monthly_pro_id_from_stripe",
amount: 19.99,
interval: "month",
},
yearly: {
id: "price_yearly_pro_id_from_stripe",
amount: 199.99,
interval: "year",
},
},
},
};
Step 3: Expose your Stripe Products and Prices to Your Frontend
Create a Cloud Function that your frontend can call to retrieve product information:
import { onRequest } from "firebase-functions/v2";
import { getSubscriptionPlans } from "./constants";
export const getStripeProducts = onRequest((_request, response) => {
response.json({
products: getSubscriptionPlans,
});
});
Step 4: Create a Stripe Checkout Session Cloud Function
Next, we want users to be able to start a Checkout Session where they enter payment informations. To do so, create a .env file in your functions folder where you add the stripe secret key like this:
STRIPE_SECRET_KEY=stripe-sk-foobar12345
Then, implement a Cloud Function that creates a Stripe checkout session when a user wants to subscribe:
import { getAuth } from "firebase-admin/auth";
import { onCall, HttpsError } from "firebase-functions/v2/https";
import Stripe from "stripe";
export const createSubscriptionCheckout = onCall(async (request) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2025-01-27.acacia",
});
try {
const { priceId, successUrl, cancelUrl } =
request.data as CreateCheckoutSessionRequest;
const auth = getAuth();
const user = await auth.getUser(request.auth?.uid || "");
if (!user.email) {
throw new HttpsError(
"failed-precondition",
"User must have an email to create a subscription",
);
}
// Get or create Stripe customer
const customerId = await getOrCreateStripeCustomer(
stripe,
user.uid,
user.email,
);
// Create checkout session
const checkoutUrl = await createCheckoutSession(
stripe,
customerId,
priceId,
successUrl,
cancelUrl,
);
return { url: checkoutUrl };
} catch (error: unknown) {
logger.error("Error creating subscription checkout:", error);
throw new HttpsError(
"internal",
error instanceof Error
? error.message
: "An error occurred while creating the checkout session",
);
}
});
Step 5: Connect Your Frontend to Stripe
In our frontend, we want to display the products and create a checkout session when users click to subscribe. For this, you could create a React component that looks something like this (not checked if this runs, if it does not, comment below please):
import React, { useState, useEffect } from 'react';
import { httpsCallable } from 'firebase/functions';
import { functions } from './firebaseConfig';
const SubscriptionPage = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch products from your Cloud Function
fetch('/api/getStripeProducts')
.then(response => response.json())
.then(data => {
setProducts(data.products);
setLoading(false);
});
}, []);
const handleSubscribe = async (priceId) => {
try {
const createCheckoutSession = httpsCallable(functions, 'createSubscriptionCheckout');
const { data } = await createCheckoutSession({
priceId,
successUrl: `${window.location.origin}/subscription/success`,
cancelUrl: `${window.location.origin}/subscription/canceled`,
});
// Redirect to Stripe Checkout
window.location.href = data.url;
} catch (error) {
console.error('Error creating checkout session:', error);
}
};
if (loading) return <div>Loading subscription plans...</div>;
return (
<div className="subscription-container">
<h1>Choose your plan</h1>
<div className="plans-grid">
{Object.values(products).map(product => (
<div key={product.id} className="plan-card">
<h2>{product.name}</h2>
<p>{product.description}</p>
<div className="pricing-options">
{Object.values(product.prices).map(price => (
<button
key={price.id}
onClick={() => handleSubscribe(price.id)}
className="subscribe-button"
>
Subscribe ${price.amount}/{price.interval}
</button>
))}
</div>
</div>
))}
</div>
</div>
);
};
export default SubscriptionPage;
Step 6: Create a Webhook Handler for Stripe Events
After a user has successfully completed their purchase, we need to update our Firestore database to reflect the new subscription status. We also need to verify the stripe-signature header to make sure that the request actually comes from stripe.
For this, we should create a webhook to process subscription events from Stripe and update your Firestore database accordingly:
import * as logger from "firebase-functions/logger";
import { onRequest } from "firebase-functions/v2/https";
import Stripe from "stripe";
import { handleSubscriptionUpdated } from "./handleSubscriptionUpdated";
export const stripeWebhook = onRequest(async (request, response) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2025-01-27.acacia",
});
const sig = request.headers["stripe-signature"];
if (!sig) {
response.status(400).send("Missing stripe-signature header");
return;
}
try {
const event = stripe.webhooks.constructEvent(
request.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET || "",
);
// Handle the event
switch (event.type) {
case "customer.subscription.created":
case "customer.subscription.updated":
case "customer.subscription.deleted":
case "customer.subscription.paused":
case "customer.subscription.resumed":
await handleSubscriptionUpdated(event);
break;
default:
logger.info(`Unhandled event type ${event.type}`);
}
response.json({ received: true });
} catch (err: unknown) {
logger.error("Error processing webhook:", err);
response
.status(400)
.send(
`Webhook Error: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
});
You can either test this webhook locally with the Stripe CLI or deploy it to Firebase and add the webhook URL to your Stripe account.
Documentation on how to test this locally can be found here.
And here's the implementation of the handleSubscriptionUpdated
function:
import type Stripe from "stripe";
import { getFirestore } from "firebase-admin/firestore";
import * as logger from "firebase-functions/logger";
import { getSubscriptionPlans } from "../config/constants";
const db = getFirestore();
export async function handleSubscriptionUpdated(event: Stripe.Event) {
const subscription = event.data.object as Stripe.Subscription;
const customerId = subscription.customer as string;
// Get the Firebase user ID from Stripe metadata
const customerData = await stripe.customers.retrieve(customerId);
const userId = customerData.metadata.firebaseUID;
if (!userId) {
logger.error("No Firebase UID found in customer metadata");
return;
}
const db = getFirestore();
// Update user subscription data in Firestore
await db
.collection("users")
.doc(userId)
.update({
subscription: {
id: subscription.id,
status: subscription.status,
priceId: subscription.items.data[0].price.id,
productId: subscription.items.data[0].price.product,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
createdAt: new Date(subscription.created * 1000),
updatedAt: new Date(),
},
});
logger.info(`Updated subscription for user ${userId}`);
}
Step 7: Create a Customer Portal Function
Even if your service is amazing, some users will inevitably cancel their subscriptions. Or maybe just update their payment method. But we need some kind of subscription management. For this, we have to create a links so they can manage their subscription through Stripe. Stripe calls this a "Customer Portal session".
Implement a function that creates a Stripe Customer Portal session to allow users to manage their subscriptions:
export const createCustomerPortal = onCall(async (request) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2025-01-27.acacia",
});
try {
const { returnUrl } = request.data as ManageSubscriptionRequest;
const auth = getAuth();
const user = await auth.getUser(request.auth?.uid || "");
if (!user.email) {
throw new HttpsError(
"failed-precondition",
"User must have an email to access the customer portal",
);
}
// Get or create Stripe customer
const customerId = await getOrCreateStripeCustomer(
stripe,
user.uid,
user.email,
);
// Create portal session
const portalUrl = await createPortalSession(stripe, customerId, returnUrl);
return { url: portalUrl };
} catch (error: unknown) {
logger.error("Error creating customer portal session:", error);
throw new HttpsError(
"internal",
error instanceof Error
? error.message
: "An error occurred while creating the portal session",
);
}
});
Step 8: Retrieve Stripe Subscription Data via Cloud Function
Create a function that allows users to check their current subscription status:
export const getSubscription = onCall(async (request) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2025-01-27.acacia",
});
const auth = getAuth();
const user = await auth.getUser(request.auth?.uid || "");
if (!user.email) {
throw new HttpsError(
"failed-precondition",
"User must have an email to get subscription details",
);
}
// Get or create Stripe customer
const customerId = await getOrCreateStripeCustomer(
stripe,
user.uid,
user.email,
);
// Get subscription details
const subscription = await getCustomerSubscription(stripe, customerId);
return { subscription };
});
Step 9: Frontend Integration of Stripe Subscription Data and Management
You should now integrate both the createCustomerPortal
and getSubscription
functions into your frontend and show this information to your user and offer the option to manage their subscription, if they have an active subscription.
Step 10: Update Firestore Rules
Of course, we need to make sure that no users with ill intent can just tell your system that they have a valid subscription. For this, we need to restrict the access to the subscription
attribute of our /user/{userId}
document. This can be done by Firestore rules, which I hope you have configured properly already! If you did not, I have a tutorial available on basic Firestore rules.
Extend your Firestore rules with this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if request.auth.uid == userId;
allow update: if request.auth.uid == userId && request.resource.data.keys().hasOnly(['name', 'email', 'preferences']);
}
match /{document=**} {
allow read, write: if false;
}
}
}
This will only allow update
operations on the name, email and preferences attributes of your user document - this is what I use to hold various informations, but you get the idea.
Step 11: Test everything and enable live mode
After deploying your functions and your frontend, test everything carefully. Everything! Subscription cancel cases, payment updates, aborted checkout sessions etc. If everything works and you're happy with it, disable Stripe test mode and you are ready to process your first subscriptions!
Conclusion
As you can see, you really only need a handful of new Firebase Cloud Functions to integrate Stripe with Firebase applications.
We have created functions to retrieve product and pricing information, create a checkout session, handle Stripe webhooks, create a customer portal session, and retrieve subscription data. We also updated Firestore rules to secure the subscription data.
I hope this guide was helpful to you and your products and that your launch is a great success! Feel free to share whatever you are building down below, I am interested to see your projects!
Top comments (0)