DEV Community

Cover image for Implementing Stripe Subscriptions with Firebase Cloud Functions and Firestore
Aron Schüler
Aron Schüler

Posted on • Originally published at aronschueler.de

1 1 1 1

Implementing Stripe Subscriptions with Firebase Cloud Functions and Firestore

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",
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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,
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"}`,
      );
  }
});
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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",
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

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 };
});
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

Image of Quadratic

AI, code, and data connections in a familiar spreadsheet UI

Simplify data analysis by connecting directly to your database or API, writing code, and using the latest LLMs.

Try Quadratic free

Top comments (0)

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed
  • 2:34 --only-changed
  • 4:27 --repeat-each
  • 5:15 --forbid-only
  • 5:51 --ui --headed --workers 1

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay