Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. GitHub. VS Code Editor. Node. Yarn. TypeScript. TSLint. Next.js. Environmental variables.
  3. Material-UI. Theme. Dark theme. Shared layout. Shared styles. Shared components. Mobile browser.
  4. HTTP. APP server. Next-Express server. Fetch method. API methods. async/await. API server. Express server. Environmental variables. Logs.
  5. User model. Mongoose and MongoDB. MongoDB index. Jest testing. Your Settings page. File upload to AWS S3.
  6. Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.
  7. AWS SES API. Passwordless OAuth API. Mailchimp API.
  8. Application state, App HOC, store and MobX. Data store for User. Toggle theme API. Team. Invitation.
  9. Discussion API. Post API. Websockets.
  10. Stripe API and paid subscription. Email notification for new post. AWS Lambda. AWS API Gateway.
  11. Environmental variables, production/development. Logger. API server. Server-side caching. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. AWS Elastic Beanstalk.

Chapter 9: Stripe API and paid subscription. Email notification for new post. AWS Lambda. AWS API Gateway.

We periodically update code and book's content.


The section below is a preview of SaaS Boilerplate Book. To read the full text, you will need to purchase the book.


In Chapter 9, you will start with the codebase in the 8-begin folder of our saas repo and end up with the codebase in the 9-end folder.

We will cover the following topics in this chapter:

  • Stripe API - API project
    - Stripe setup - API
    - Stripe methods - API
    - Stripe checkout callback - API
    - Stripe webhook - API
    - Team Model - API
    - User Model - API
    - Team Leader Express routes - API

  • Stripe API - APP project
    - Team Leader API methods - APP
    - Team data store - APP
    - User data store - APP
    - Billing page and testing - APP
    - TeamSettings page and testing - APP

  • Setup at Stripe dashboard and environmental variables
    - Testing Stripe API

  • Email notification for new post API - API project
    - Discussion and EmailTemplate Models - API
    - Express routes for adding and editing Discussion - API
    - sendDataToLambdaApiMethod API method - APP
    - Discussion data store - APP
    - CreateDiscussionForm, EditDiscussionForm and PostForm - APP
    - Testing updates to internal API infrastructure

  • Amazon API Gateway and AWS Lambda
    - Amazon API Gateway
    - AWS Lambda
    - Testing entire Email notification for new post API


link Stripe API - API project

In this section we will discuss subscriptions and how to implement them using Stripe API. We will outline entire Stripe API infrastructure for our application. Any SaaS product has to have a way to charge customers. Most non-enterprise SaaS businesses charge customers on a monthly basis, somewhere in $25-$500 range. In this book we will show to implement following features in our SaaS boilerplate so that Team Leader end user can:
- buy subscription (subscribe to monthly plan)
- unsubscribe (stop subscription)
- update card information
- see list of all invoices for subscription payments

In your real world SaaS product you are decision maker on what features to provide to your customers in terms of billing. In this section we will build a simple Billing page that allows end user to do the above for actions:

Builder Book

Besides figuring out what billing features to add to your SaaS product, you should decide when to ask customers to buy a monthly plan. Your particular SaaS product may offer trial period or require all customers to become paying customers right from the beginning. For trial period you should decided whether you want to collect card information and whether customers becomes paying customer automatically at the end of the trial period. By knowing your customers well, you can decide on would work better for them and for your business.

In this book, we are showing you, perhaps, less aggressive approach of offering customers to pay for your SaaS product. We let any end user to use our boilerplate but set limit to number of team members Team Leader can invite to team. By this point in the book you have two accounts, one for Team Leader and one for Team Member. We will set up limit of two team members including Team Leader. In other words, Team Leader has to become paying customer in order to add third team member to team:

Builder Book

Team Leader Potato has to go to Billing page, buy a subscription in order to be able to add a third member to the Team Builder Book team.

We will be using a newer Stripe API that prescribes using Stripe Checkout that redirects potential customer to Stripe-hosted checkout page to add payment information and confirm payment. This newer API also prescribes creating a Stripe session object that contains information about customer, subscription and redirect URLs for different outcomes among other data. These three possible outcomes have different redirect URLs. Team Leader in our application will click on Buy Subscription button and be redirected to Stripe Checkout Website. Then end user can either cancel or proceed with payment. If end user cancels, then end user gets redirected to Billings page and sees cancel message. If end user proceeds and error is caught, end user gets redirected back to Billings page and our application shows end user an error message. Finally, if end user proceeds and payment is successful, end user gets redirected to Billings page and see an updated UI that shows that end user is indeed paying customer, our application has end user's card saved and end user can see a first invoice on the list of invoices.

We can summarize Stripe API infrastructure in our application like this:

Builder Book


link Stripe setup - API

As we mentioned a bit earlier we will be using most recent API approach as of August of 2020. The older approach was using a Stripe popup within your application. The newer approach prescribes us to use checkout page hosted by Stripe, Stripe Checkout:

https://stripe.com/docs/payments/checkout

Checkout is checkout page that works across devices including mobile browsers. Our application has to redirect potential customer to Checkout and then back to our application.

For every attempt to manually pay in our application Stripe prescribes creating and retrieving Session object:

https://stripe.com/docs/api/checkout/sessions

Stripe Session should be created for single payments and first subscription payment. In our case we use it for first subscription payment. We need to create Session object on our API server, then pass Session's id sessionId to APP project on browser and call redirectToCheckout({ sessionId }) to redirect end user from our Billing page to Stripe's Checkout page.

https://stripe.com/docs/js/checkout/redirect_to_checkout

When API server gets request from Stripe server - API has to run some code to update corresponding Team and User MongoDB documents. For example, we want to set field isSubscriptionActive to true for Team document and save stripeCard object as a field to User document. How do we find a correct Team and User to update? We retrieve Session object and contains all necessary information to find correct Team and User documents.

Here is a visual explanation when we need to create and retrieve Session. We need to create Session before redirect to Checkout. We need retrieve Session after getting request from Stripe server:

Builder Book

Go to Stripe API documentation and find create a Session API method:

https://stripe.com/docs/api/checkout/sessions/create

Select Node.js in the Select library dropdown:

Builder Book

As you can see the Stripe API method we are looking for is stripe.checkout.sessions.create.

Go to Stripe API documentation and find Stripe API method to retrieve a Session:

https://stripe.com/docs/api/checkout/sessions/retrieve

Builder Book

The Stripe API method we are looking for is stripe.checkout.sessions.retrieve.

Let's define createSession on our API server. We, as developers, can specify shape of Session object however some properties are required. For example, payment_method_types, success_url and cancel_url are required properties of Session object. We want to save userId and teamId to be able to find User and Team documents later and update them.

createSession Stripe method:

const dev = process.env.NODE_ENV !== 'production';

const stripeInstance = new Stripe(
    dev ? process.env.STRIPE_TEST_SECRETKEY : process.env.STRIPE_LIVE_SECRETKEY,
    { apiVersion: '2020-03-02' },
);

function createSession({ userId, teamId, teamSlug, customerId, subscriptionId, userEmail, mode }) {
    const params: Stripe.Checkout.SessionCreateParams = {
        customer_email: customerId ? undefined : userEmail,
        customer: customerId,
        payment_method_types: ['card'],
        mode,
        success_url: `${process.env.URL_API}/stripe/checkout-completed/{CHECKOUT_SESSION_ID}`,
        cancel_url: `${process.env.URL_APP}/team/${teamSlug}/billing?         redirectMessage=Checkout%20canceled`,
        metadata: { userId, teamId },
    };

    if (mode === 'subscription') {
        params.line_items = [
            {
                price: dev ? process.env.STRIPE_TEST_PRICEID : process.env.STRIPE_LIVE_PRICEID,
                quantity: 1,
            },
        ];
    } else if (mode === 'setup') {
        if (!customerId || !subscriptionId) {
            throw new Error('customerId and subscriptionId required');
        }

        params.setup_intent_data = {
            metadata: { customer_id: customerId, subscription_id: subscriptionId },
        };
    }

    return stripeInstance.checkout.sessions.create(params);
}

Once we defined params we called Stripe API method stripeInstance.checkout.sessions.create(params). We should discuss a few parts of method in more detail:
- The type of params is Stripe.Checkout.SessionCreateParams. You should have stripe package already installed so Stripe.Checkout.SessionCreateParams is available.
- success_url has API endpoint that means we have to define matching Express route /stripe/checkout-completed/:sessionId. Then we access sessionId as req.params.sessionId and retrieve Session object, update Team and User documents and send res.redirect to browser to redirect end user from Checkout back to our application. Either Billing page with success UI or Billing page with error message via redirectMessage prop and notify.
- cancel_url is straightforward to understand req.query.redirectMessage is accessed by our code inside App.getInitialProps and populated as redirectMessage prop in all pages. We need to call notify on the Billing page to show end user cancel message. - mode can have two values in our application. For making first subscription payment and becoming subscriber, value for mode is subscription. For updating card information, value of mode is setup: https://stripe.com/docs/payments/checkout/subscriptions/update-payment-details#create-checkout-session
- When mode value is setup we have to check for customerId and subscriptionId to make sure Stripe customer exists and Stripe Subscription exists. Makes no sense to let user to edit payment method.

Stripe method retrieveSession takes one argument and expands properties of Session object it retrieves with Stripe API method stripeInstance.checkout.sessions.retrieve:

function retrieveSession({ sessionId }: { sessionId: string }) {
    return stripeInstance.checkout.sessions.retrieve(sessionId, {
        expand: [
            'setup_intent',
            'setup_intent.payment_method',
            'customer',
            'subscription',
            'subscription.default_payment_method',
        ],
    });
}

Expanding response in Stripe API means getting an additional data, in this case object's properties:

https://stripe.com/docs/api/expanding_objects

Builder Book

Place above code into new file book/9-begin/api/server/stripe.ts. At the top of the file add line that disable camelcase rule (for example, success_url parameter is not camelcase) and imports:

/* eslint-disable @typescript-eslint/camelcase */

import * as bodyParser from 'body-parser';
import Stripe from 'stripe';

import Team from './models/Team';
import User from './models/User';

const dev = process.env.NODE_ENV !== 'production';

const stripeInstance = new Stripe(
    dev ? process.env.STRIPE_TEST_SECRETKEY : process.env.STRIPE_LIVE_SECRETKEY,
    { apiVersion: '2020-03-02' },
);

function createSession({ userId, teamId, teamSlug, customerId, subscriptionId, userEmail, mode }) {
    const params: Stripe.Checkout.SessionCreateParams = {
        customer_email: customerId ? undefined : userEmail,
        customer: customerId,
        payment_method_types: ['card'],
        mode,
        success_url: `${process.env.URL_API}/stripe/checkout-completed/{CHECKOUT_SESSION_ID}`,
        cancel_url: `${process.env.URL_APP}/team/${teamSlug}/billing?redirectMessage=Checkout%20canceled`,
        metadata: { userId, teamId },
    };

    if (mode === 'subscription') {
        params.line_items = [
            {
                price: dev ? process.env.STRIPE_TEST_PRICEID : process.env.STRIPE_LIVE_PRICEID,
                quantity: 1,
            },
        ];
    } else if (mode === 'setup') {
        if (!customerId || !subscriptionId) {
            throw new Error('customerId and subscriptionId required');
        }

        params.setup_intent_data = {
            metadata: { customer_id: customerId, subscription_id: subscriptionId },
        };
    }

    return stripeInstance.checkout.sessions.create(params);
}

function retrieveSession({ sessionId }: { sessionId: string }) {
    return stripeInstance.checkout.sessions.retrieve(sessionId, {
        expand: [
            'setup_intent',
            'setup_intent.payment_method',
            'customer',
            'subscription',
            'subscription.default_payment_method',
        ],
    });
}

link Stripe methods - API

In this subsection we will define four Stripe methods:
- updateCustomer and updateSubscription Stripe methods will be used inside Express route /stripe/checkout-completed/:sessionId that we are yet to define
- cancelSubscription Stripe method will be used inside static methods cancelSubscription and cancelSubscriptionAfterFailedPayment of Team model
- getListOfInvoices Stripe method will be used inside static method getListOfInvoicesForCustomer of User model

In previous subsection you have familiarized yourself with Stripe API documentation. Inside Express route /stripe/checkout-completed/:sessionId we want to use updateCustomer and updateSubscription Stripe methods to update Stripe Customer's and Stripe Subscription's default_payment_method parameter. After checking Stripe API docs:

https://stripe.com/docs/api/customers/update

https://stripe.com/docs/api/subscriptions/update

You can find that Stripe API methods we should use are stripeInstance.customers.update and stripeInstance.subscriptions.update.

Inside cancelSubscription we want to cancel Subscription:

https://stripe.com/docs/api/subscriptions/cancel

Thus we use stripeInstance.subscriptions.del Stripe API method.

And we want getListOfInvoices to return list of invoices:

https://stripe.com/docs/api/invoices/list

Thus we use stripeInstance.invoices.list Stripe API method.

Add four new Stripe methods to book/9-begin/api/server/stripe.ts file under retrieveSession method:


You've reached the end of the Chapter 9 preview. To continue reading, you will need to purchase the book.

We periodically update code and book's content.


format_list_bulleted
help_outline
lens