Processing Payments in Firebase with Stripe

A simple guide to using the Run payments with Stripe extension in a Firebase project

Lukas Kuppers
Firebase Developers
6 min readSep 4, 2023

--

In this article I will show how to quickly process payments in a Firebase project using Stripe and the firestore-stripe-payments extension. The extension docs encourage users to install the JS package, but I took a different approach which I will go through in the guide below.

Covered in this guide:

Not covered in this guide:

  • Making mobile payments
  • Setting up authentication for a Firebase app
  • Handling trials
  • Other payment aspects (taxes, custom metadata, multiple prices)

Background

If you’re serious about monetizing your Firebase app, you’ve probably looked into getting some sort of payment system set up. The Run Payments with Stripe extension is a great solution.

Note: Ownership of the Stripe extension has been transferred to Invertase. That means that you need to use the Invertase version of the Run Payments with Stripe extension, as the Stripe version will no longer receive maintenance updates!

When setting up the Stripe payments extension personally, I found hard to navigate the documentation. I plan to solve this issue by walking you through the basic process of how to get Stripe payments working in a Firebase app. In addition, you might find this example project useful in getting Stripe working. Also, you can view the live version of the app I was building with Firebase and Stripe here —it might come in handy.

1. Install and configure the extension

I’m going to assume you already have a Firebase project, and know the basis of Firebase.

  1. Setup a Stripe account — you can create a new account there and turn on test mode.
  2. Install the firestore-stripe-payments extension in your project. (Note: you need to upgrade to the Blaze plan in Firebase)
  3. Configure the extension — Everything here is pretty self-explanatory, just follow the instructions. You can always change the config later. Also, when just getting started out, you can keep events disabled.

Note: If your restricted API key and webhook were obtained in Stripe test mode, you’ll need to update these values when you go live in Stripe.

In general, configuring the extension is quite easy, but I’ll highlight three critical steps:

  1. Copy the provided Cloud Firestore security rules into your projects security rules (Firestore Database -> Rules):
match /databases/{database}/documents {
match /stripe-customers/{uid} {
allow read: if request.auth.uid == uid;

match /checkout_sessions/{id} {
allow read, write: if request.auth.uid == uid;
}
match /subscriptions/{id} {
allow read: if request.auth.uid == uid;
}
match /payments/{id} {
allow read: if request.auth.uid == uid;
}
}

match /stripe-products/{id} {
allow read: if true;

match /prices/{id} {
allow read: if true;
}

match /tax_rates/{id} {
allow read: if true;
}
}
}

2. Configure the Stripe webhook as outlined in the provided docs. Nothing will work if this isn’t setup properly. And again, you’ll need to re-enter the Stripe webhook secret when switching Stripe from test to live mode.

3. Make sure Firebase Authentication is enabled for your project. Users should need to sign in before they can make payments.

2. Get Stripe products in your Web app

You’re going to need to fetch product info in your frontend app, even if you won’t be displaying product info from Stripe, because you need the Stripe price ID to initiate payment sessions.

Here’s how you can get all the active products (and their prices) you created in Stripe. Note that db is the Firestore object obtained using getFirestore():

import { query, collection, where, getDocs } from 'firebase/firestore';

// create a query object
const q = query(
collection(db, 'products'),
where('active', '==', true)
);

const querySnapshot = await getDocs(q);

// for each product, get the product price info
const productsPromises = querySnapshot.docs.map(async (productDoc) => {
let productInfo = productDoc.data();

// fetch prices subcollection per product
const pricesCollection = collection(productDoc.ref, 'prices');
const priceQuerySnapshot = await getDocs(pricesCollection);

// assume there is only one price per product
const priceDoc = priceQuerySnapshot.docs[0];
productInfo['priceId'] = priceDoc.id;
productInfo['priceInfo'] = priceDoc.data();
return productInfo;
});

// 'products' is an array of products (including price info)
const products = await Promise.all(productsPromises);

3. Redirect users to a Stripe checkout session

Once you’ve fetched product info, you can redirect users to a stripe payment page: You will NOT be using payment links from the Stripe dashboard! You would probably put this code in a function that gets triggered on a button click, or something like that.

Observe that the code is creating a new checkout session in Firestore. Then, the Stripe extension detects this and creates a payment link for us, where we can redirect the user.

Note: You can customize the look of Stripe pages here

import { addDoc, collection, onSnapshot } from 'firebase/firestore';

let checkoutSessionData = {
price: 'price_1GqIC8HYgolSBA35zoTTN2Zl', // price ID from products fetch
success_url: window.location.origin, // can set this to a custom page
cancel_url: window.location.origin // can set this to a custom page
};

// if payment is a one-time payment (as opposed to subscription)
// append mode: 'payment' to the checkout session data
if (isOneTime) {
checkoutSessionData['mode'] = 'payment';
}

const checkoutSessionRef = await addDoc(
// currentUser is provided by firebase, via getAuth().currentUser
collection(db, `customers/${currentUser.uid}/checkout_sessions`),
checkoutSessionData
);

// The Stripe extension creates a payment link for us
onSnapshot(checkoutSessionRef, (snap) => {
const { error, url } = snap.data();
if (error) {
// handle error
}
if (url) {
window.location.assign(url); // redirect to payment link
}
});

4. Get active subscriptions for the current user

In the frontend, you might want to modify the content you display to the user based on their subscription / whether or not they’re subscribed at all. You can quickly get subscriptions like this:

import { query, collection, where, getDocs } from 'firebase/firestore';

// create a query object to the current users active subscriptions
const q = query(
// currentUser is provided by firebase, via getAuth().currentUser
collection(db, 'customers', currentUser.uid, 'subscriptions'),
where('status', 'in', ['trialing', 'active'])
);

// fetch the active subscriptions
const querySnapshot = await getDocs(q);
if (querySnapshot.empty) {
// user doesn't have any active subscriptions
}
// assuming user only has one active subscription max
const subscription = querySnapshot.docs[0].data();

5. Redirect users to a Stripe customer portal session

If you have subscriptions in your app, you’ll want subscribed users to be able to manage their subscription (change payment info, cancel, etc). You can redirect users to a Stripe customer portal session like this:

import { getFunctions, httpsCallable } from 'firebase/functions';

// firebaseApp is object created using initializeApp()
// may need to change server location
const functions = getFunctions(firebaseApp, 'us-central1');
const createPortalLink = httpsCallable(
functions,
'ext-firestore-stripe-payments-createPortalLink');

// request Stripe to create a portal link, and redirect user there
createPortalLink({
returnUrl: window.location.origin // can set this to a custom page
}).then((result) => {
window.location.assign(result.data.url);
}).catch((error) => {
// handle error
});

6. Handle payment events on the backend

You might want to have some custom function triggered when a successful payment goes through. For example, in my app (a public REST API), I wanted to give the user API tokens when they made a payment. To do this, you’ll need Cloud Functions set up for your project.

The Stripe Firebase extension publishes event arc events that get triggered during different phases of the payment lifecycle. To enable these events, you need to check enable events in the Stripe extension config, and select the exact events you want enabled in the dropdown. (Note: make sure to click Save after making changes to the extension config!).

You can access a log of events that have occurred in the Stripe dashboard by going to Developer->Events. If you click on an event, you can see all the data that will be available to you if you use that event. Events that I ended up using are:

  • com.stripe.v1.checkout.session.completed — a successful payment occurred
  • com.stripe.v1.customer.subscription.deleted — a users subscription ended

To handle events, insert something like following into functions/index.js . As an example, I am handling the checkout.session.complete event:

const {onCustomEventPublished} = require('firebase-functions/v2/eventarc');

// other stuff in your firebase functions

// eventarc events (for Stripe payments extension)
exports.onCheckoutSessionCompleted = onCustomEventPublished(
'com.stripe.v1.checkout.session.completed',
(event) => {
// handle event
// get the same JSON ojbect you see in the Stripe dashboard
const eventData = event.data;

// for example, check if the payment was a one time payment:
if (eventData.mode === 'payment') {
// one time payment was succesful, handle it...
}
});

Wrapping Up

If you got this far, thanks for reading — I hope this guide helped you build your awesome app.

Here are some other potentially useful links:

--

--