Serverless Subscription Management with Fauna, Paddle, Gatsby and Netlify
Tags: fauna, gatsby, serverless, netlify
Published: 2021-09-16 12:45:08

In this article I will show you how you can extend a Gatsby static-site to manage memberships and paid subscriptions using Netlify and Paddle, with Fauna’s cloud API as the glue that keeps everything together.

We will be covering the following:

* Setting up a basic Gatsby site
* Configuring and deploying to Netlify
* Membership management using Netlify Identity
* Adding login and signup modals using React hooks
* Adding Gatsby private routes
* Creating subscription plans using Paddle
* Subscription management from members’ accounts using Fauna

You can find all of the finished code for the project [here](https://github.com/gjdickens/fauna-gatsby-netlify) along with a [working demo here](https://fauna-gatsby-netlify.epilocal.com/).

Before we get started, let’s do a quick visual tour of what we will building:

![Gatsby Blog with Login Buttons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edgwnpk9z4pbe37gykso.png)

![Gatsby Blog Login Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nzmzy4qzh8zatr3qx1vg.png)

![Gatsby Blog with Membership](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yc6t7e875in6xsaccxys.png)

![Paddle with Gatsby Blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59ox4o8dnog8i17pyjpw.png)

![Gatsby Account Management](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmbuqdx4cioci33thcbk.png)

![Gatsby Manage Subscriptions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/55ogfvca2yyi4j3v0hpd.png)

## Creating your Gatsby site

To begin with, we need a basic Gatsby site to build from.

We will start by installing the Gatsby CLI (Command Line Interface) by running the command:


```
npm install -g gatsby-cli
```


Next we will spin up a Gatsby instance using their default [Gatsby Starter Blog](https://github.com/gatsbyjs/gatsby-starter-blog) starter. To do this, just go to the folder you want to install in and run:


```
gatsby new <your-project-name> https://github.com/gatsbyjs/gatsby-starter-blog
```


Now if you run `gatsby develop  `you can navigate to `http://localhost:8000/` in your browser and see your site running on a development server.

![Gatsby Starter Blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u96eb2ezz8c6f8q0pm97.jpeg)


## Deploying to Netlify

With our Gatsby site up and running our next step is to deploy it to Netlify so that we can take advantage of Netlify’s serverless functions and identity storage.  (if you don’t yet have a Netlify account [you can create one for free here](https://app.netlify.com/)).  

We will be deploying on Netlify directly from Github, so now is a good time to initialize your repository if you haven’t yet already.  Now we will add a short `netlify.toml` file in our root directory that will tell Netlify where we are going to be keeping our serverless functions:


```
[build]
  command = "NODE_ENV=production gatsby build"
  publish = "public/"
  functions = "./functions"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200
```


Here we are simply telling Netlify that on build, any serverless functions we create will be in the `./functions `folder and they will be served from the endpoint `www.mysite/api/`.

Now if we follow the Netlify instructions to link our Git repository, you will have a fully deployed site on Netlify that will be automatically rebuilt on any new commits and will be ready for our serverless functions that we will create in a little while.

While we are in Netlify, let’s also enable Netlify Identity which we will also be using soon.  To do this, go to the Netlify dashboard and select the site that we just created.  Then go to Identity and click “Enable Identity.”

![Netlify Identity](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/baidaee3g18cpkpmq7zi.png)

Finally, we will install the [Netlify plugin for Gatsby](https://www.gatsbyjs.com/plugins/gatsby-plugin-netlify/) that will handle some configuration automatically for us when it comes to our private routes.  To do that we will simply run `npm install --save gatsby-plugin-netlify `and add it to our `gatsby-config.js `file.

With that, we are all done with our Netlify configuration.  Note, from now on, if we use the command `netlify dev `instead of` gatsby develop`, we will be able to see changes to our serverless functions in real-time.  So I recommend that you use that as you are following along.


## Member management with Netlify Identity

Let’s now move on to how we can use Netlify’s Identity to manage our members.  To help us with this, we will use a Gatsby plugin called `gatsby-plugin-netlify-identity-gotrue` which is a wrapper for another package called `react-netlify-identity-gotrue`.

[GoTrue](https://github.com/netlify/gotrue) is an open-source API maintained by Netlify that allows you to access Netlify’s identity membership signup and login features.  These two packages directly integrate GoTrue into React and Gatsby allow us to easily use Netlify Identity in our project.

Let’s start by getting both of these installed with:


```
npm install --save gatsby-plugin-netlify-identity-gotrue react-netlify-identity-gotrue.
```


Next let’s add the plugin to our `gatsby-config.js:`


``` javascript
    {
      resolve: `gatsby-plugin-netlify-identity-gotrue`,
      options: {
        url: `https://YOUR-NETLIFY-APP-URL`
      }
    },
```


Now we have access to a variety of helpful functions along with the identity context of any logged in user.  Let’s start using them by creating our user login process, beginning with some login buttons that will control our signup modals using React Hooks.  

First we will create a simple `button.js` component in `src/components`:


``` javascript
import React from 'react';

import { handleKeyDown } from '../utils/utils';

const Button = ({ children, showPopup, setShowPopup }) => {

  const handleLoginClick = () => {
    setShowPopup(!showPopup);
  }

  const handleLoginKeyDown = (e) => {
    handleKeyDown(e, showPopup, setShowPopup);
  }

  return (
    <button
      onClick={ (e) => handleLoginClick() }
      onKeyDown={ (e) => handleLoginKeyDown(e) }
       >{children}</button>

  )
}

export default Button;
```


And then in a new folder `src/utils` we will create a file called `utils.js` and add this:


``` javascript
export const handleKeyDown = (ev, showPopup, setShowPopup) => {
  if (ev.keyCode === 13 && !showPopup) {
    // enter to open
    setShowPopup(true);
  } else if (ev.keyCode === 27 && showPopup) {
    // escape to close
    setShowPopup(false);
  }
}
```


In the component we are adding the ability for the button to control React Hooks that we will pass into it in order to determine whether or not our Signup/Login modal will be displayed.  While the `handleKeyDown` function is checking for keyboard enter and escape events and setting `showPopup` to `true` or `false` respectively.  We put the `handleKeyDown `function into a separate` utills.js` file since we will be using the same function later when we create our modal in order to let users close the modal using the escape key.

Next, let’s move on to create a new component file `src/components/loginButtons.js:`


``` javascript
import React from 'react';
import { Link } from "gatsby";
import { useIdentityContext } from 'react-netlify-identity-gotrue';

import Button from './button';

const LoginButtons = ({ children, showRegisterPopup, setShowRegisterPopup, showLoginPopup, setShowLoginPopup }) => {

  const identity = useIdentityContext();

  return (
    <>
      {!identity.user &&
        <>
        <div className="headerButton">
          <Button showPopup={showRegisterPopup} setShowPopup={setShowRegisterPopup}>Sign Up</Button>
          <Button showPopup={showLoginPopup} setShowPopup={setShowLoginPopup}>Login</Button>
        </div>
        </>

      }
      {identity.user &&
        <>
        <div className="headerButton">
          <button
             ><Link to="/account/profile">Profile</Link></button>
          <button
            className="link"
            onClick={identity.logout}
            onKeyDown={identity.logout}
             >Logout</button>
        </div>
        </>
      }
    </>
  )

}
export default LoginButtons;
```


At the same time, we will also make a few additions to our `src/components/layout.js` file:


``` javascript
import React, { useState } from 'react';
import { Link } from "gatsby"

import LoginButtons from './loginButtons';  

const Layout = ({ location, title, children }) => {
  const rootPath = `${__PATH_PREFIX__}/`;
  const isRootPath = location.pathname === rootPath
  let header

  const[showRegisterPopup, setShowRegisterPopup] = useState(false);
  const[showLoginPopup, setShowLoginPopup] = useState(false);

  if (isRootPath) {
    header = (
      <>
        <h1 className="main-heading">
          <Link to="/">{title}</Link>
        </h1>
        <LoginButtons showRegisterPopup={showRegisterPopup} setShowRegisterPopup={setShowRegisterPopup} showLoginPopup={showLoginPopup} setShowLoginPopup={setShowLoginPopup} />
      </>
    )
  } else {
    header = (
      <>
        <Link className="header-link-home" to="/">
          {title}
        </Link>
        <LoginButtons showRegisterPopup={showRegisterPopup} setShowRegisterPopup={setShowRegisterPopup} showLoginPopup={showLoginPopup} setShowLoginPopup={setShowLoginPopup} />
      </>
    )
  }
```


In our component,  we are initializing our Netlify Identity context helper.  With this we can check if a user is logged in by checking if `identity.user` exists.  If it doesn’t, we will show two buttons:  one for signing up and one for logging in.

![Gatsby Blog with Login Buttons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edgwnpk9z4pbe37gykso.png)

These buttons will toggle the state that is controlled in the `layout.js `file using the React Hooks `showRegisterPopup` and `showLoginPopup`.  We will manage this state from the Layout component since we will also have to pass these hooks into the modals themselves so we can close them.

When a user is logged in, we will show him a link to his profile page (we will create this a bit later using Gatsby private routes), as well as the option to log out.   Using our Netlify Identity context helper we can simply call `identity.logout` which will handle all the backend of the logout process for us.

Now we need to create our actual modal, which we will do in two parts:  First, the popup itself in a new component called `popup.js` and after that the form component that we will call `registerForm.js` which will be served through the popup and will be used to collect sign up and login information.

Let’s look at the popup modal first:


``` javascript
import React from 'react';
import { X } from 'react-feather';

import { handleKeyDown } from '../utils/utils';

import './styles/popup.css';

const Popup = ( { children, showPopup, setShowPopup } ) => {

  return (
    <>
      {showPopup ? (
        <div className="popup-overlay">
          <div
            className="popup-background"
            onClick={ (e) => setShowPopup(false) }
            onKeyDown={ (e) => handleKeyDown(e, showPopup, setShowPopup) }
            tabIndex={0}
            aria-label="Toggle Popup"
            role="button"
          ></div>
          <div className="popup-inner">
            <X
              className="popup-close"
              onClick={ (e) => setShowPopup(false) }
              onKeyDown={ (e) => handleKeyDown(e, showPopup, setShowPopup) }
              tabIndex={0}
              aria-label="Toggle Popup"
              role="button"
            />
            {children}
          </div>
        </div>
      ) : null}
    </>
  )
}
export default Popup;
```


First, note that we are importing a custom styles file here as well as the icons package `react-feather`.  You can find the CSS file in the source code for the example [here](https://github.com/gjdickens/fauna-gatsby-netlify/blob/master/src/components/styles/popup.css) and simply copy and paste it into your project, while `react-feather` can be installed with:


```
npm install --save react-feather
```


Styling aside, you will see that we are passing in the React hook `showPopup` which will control whether or not anything is actually returned.  Then you will see that if someone clicks (or presses the escape key) on either the background overlay of the modal or the X icon, `showPopup` will be set to false and the modal will be hidden again.  Finally, you will see that we pass in children to allow us to display our form inside the popup.

Let’s create the component for that form in  `src/components/registerForm.js`:


``` javascript
import React, { useState } from 'react';
import { useIdentityContext } from 'react-netlify-identity-gotrue';

const RegisterForm = ({ children, loginType, setShowPopup }) => {
  const identity = useIdentityContext();

  const [formValues, setFormValues] = useState({
  	email: '',
    password: ''
  });

  const handleEmailInputChange = (event) => {
  	setFormValues((values) => ({
  		...values,
  		email: event.target.value,
  	}));
  };

  const handlePasswordInputChange = (event) => {
    setFormValues((values) => ({
      ...values,
      password: event.target.value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (loginType === 'login') {
      handleLogin(formValues);
    }
    else {
      handleSignup(formValues);
    }

  }

  const handleSignup = async ( formValues ) => {
    let newUser = {
      email: formValues.email,
      password: formValues.password
    }

    await identity.signup(newUser)
    .then(() => {
      console.log('registration successful');
      setShowPopup(false);
    })
    .catch((e) => {
      console.log(e);
    });

  }

  const handleLogin = async ( formValues ) => {
      await identity.login({
        email: formValues.email,
        password: formValues.password
      })
    .then(() => {
      setShowPopup(false);
    })
    .catch((e) => {
      console.log(e);
    });

  }

  return (

    <div className="member-form">
      <form onSubmit={handleSubmit}>
        <div className="member-form-group">
          <label htmlFor="username">Email</label>
          <input type="email" className='member-input' placeholder="youremail@example.com" value={formValues.email} onChange={handleEmailInputChange} />
        </div>
        <div className="member-form-group">
          <div>
            <label htmlFor="password">Password</label>
            <input type="password" className='member-input' placeholder="Password" value={formValues.password} onChange={handlePasswordInputChange} />
          </div>
        </div>

        <div className="member-form-group" style={{textAlign: 'center', paddingTop: '2rem'}}>
          <button className="button member-submit" type="submit">
            <span className="button-content">{loginType ==='login' ? 'Login' : 'Register'}</span>
          </button>
        </div>
      </form>
    </div>
)

}

export default RegisterForm;
```


Here we are using helper functions from our Netlify Identity context in order to submit our form data.  Let’s leave those to the side for a minute while we see how the form itself is actually working.

We have a React hook called `formValues` which is initiated with empty values for “email” and “password.”  Whenever one of the inputs on the form changes, these values are updated via `setFormValues `which is called from both `handleEmailInputChange` and `handlePasswordInputChange`. Finally, when the form is submitted the function `handleSubmit` is called which checks to see whether this is a signup or login form based on the property we have passed in called `loginType.`

Now let’s look at where we use the Netlify helper functions.  First, we can see in `handleSignup` that we are getting the form values and storing them in an object that has an “email” and a “password” key.  Then we simply need to provide this object to `identity.signup()` and Netlify automatically logs the user in for us.  (as well as creating a cookie that persists the login state)  We use `await` here so that when the function returns successfully we can close the popup modal automatically by calling `setShowPopup(false)`.

If we look at `handleLogin`, we see that it is very similar.  The only difference is that now we are calling `identity.login()` instead.  One thing to note is that since we are using `await`, both of these functions need to be `async` functions.

Finally, we just need to wire up our popup modal and form in our `layout.js` file.  Here’s the updated version:


``` javascript
import React, { useState } from 'react';
import { Link } from "gatsby"

import Popup from './popup';
import RegisterForm from './registerForm';
import LoginButtons from './loginButtons';

const Layout = ({ location, title, children }) => {
  const rootPath = `${__PATH_PREFIX__}/`;
  const isRootPath = location.pathname === rootPath
  let header

  const[showRegisterPopup, setShowRegisterPopup] = useState(false);
  const[showLoginPopup, setShowLoginPopup] = useState(false);

  header = (
    <>
      {isRootPath ?
        <h1 className="main-heading">
          <Link to="/">{title}</Link>
        </h1>
         :
         <Link className="header-link-home" to="/">
           {title}
         </Link>
       }
      <LoginButtons showRegisterPopup={showRegisterPopup} setShowRegisterPopup={setShowRegisterPopup} showLoginPopup={showLoginPopup} setShowLoginPopup={setShowLoginPopup} />
      <Popup showPopup={showRegisterPopup} setShowPopup={setShowRegisterPopup} >
        <h1 style={{textAlign: 'center', paddingBottom: '2rem'}}>Join now</h1>
        <RegisterForm loginType='register' setShowPopup={setShowRegisterPopup} />
      </Popup>
      <Popup showPopup={showLoginPopup} setShowPopup={setShowLoginPopup} >
        <h1 style={{textAlign: 'center', paddingBottom: '2rem'}}>Sign in to access your account</h1>
        <RegisterForm loginType='login' setShowPopup={setShowLoginPopup} />
      </Popup>
    </>
  )

  return (
    <div className="global-wrapper" data-is-root-path={isRootPath}>
      <header className="global-header">{header}</header>
      <main>{children}</main>
      <footer>
        © {new Date().getFullYear()}, Built with
        {` `}
        <a href="https://www.gatsbyjs.com">Gatsby</a>
      </footer>
    </div>
  )
}

export default Layout;
```


First, note that I’ve refactored the code of the original template in order to avoid duplication in the header component.  Instead of the previous `If / else` statement, I’ve put a conditional statement directly in the HTML to control what is displayed when `isRootPath` is true.

Then after that we simply import our Popup and RegisterForm components and pass in the React hooks for signing up and logging in, as well as a title for each popup.  Our finished popup will now look like this:

![Gatsby Blog Login Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nzmzy4qzh8zatr3qx1vg.png)


## Gatsby Private Routes

Next we will set up private routes in Gatsby that will allow us to have a private account management section of the site for each user.  

Essentially, a private route in Gatsby is a route that is only generated on client-side when certain conditions are met.  (rather than the majority of a Gatsby site, or any other static site for that matter, that will be generated in advance on the server side)  This prevents someone who is not entitled to see some content from being served it along with all of the other resources of a site.  You can [read more about it here](https://www.gatsbyjs.com/docs/how-to/routing/client-only-routes-and-user-authentication/).  

There’s also a great tutorial on how to get this setup using Auth0 instead of Netlify Identity over on the [Auth0 blog](https://auth0.com/blog/securing-gatsby-with-auth0/).  (credit to the author [Sam Julien](https://www.samjulien.com/), as a lot of the following code was adapted from his examples)

To get this set up, we first need to edit our `gatsby-node.js` file in the root of our project. We will be adding the following code to what’s already there:


``` javascript
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions

  if (page.path.match(/^\/account/)) {
    page.matchPath = "/account/*"

    createPage(page)
  }
}
```


What this is doing is it is telling Gatsby to look for any pages that fall under the path `/account/` and that these pages should be rendered on the client-side only. (rather than generated as static content on the server side.

With that done, we can now create our account page as a new file in the page directory called `account.js`:


``` javascript
import React from "react"
import { Link, graphql } from "gatsby"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/profile"
import PrivateRoute from "../components/privateRoute"

const Account = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`

  return (
    <Layout location={location} title={siteTitle}>
      <Router>
        <PrivateRoute path="/account/profile" component={Profile} />
      </Router>
    </Layout>
  )
}

export default Account

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      nodes {
        excerpt
        fields {
          slug
        }
        frontmatter {
          date(formatString: "MMMM DD, YYYY")
          title
          description
        }
      }
    }
  }
`
```


Most of this is boilerplate and copied from our `index.js` page, including the GraphQL query and the metadata that is passed into our Layout component.  The important thing to notice here is that we are using Router imported from `@reach/router`  (no NPM install required since it comes by default with Gatsby) to wrap a new component called PrivateRoute that we will create now.

So let’s create a new file in our components folder called `privateRoute.js`:


``` javascript
import React from "react";
import { useIdentityContext } from 'react-netlify-identity-gotrue';

const PrivateRoute = ({ component: Component, path, ...rest }) => {
  const identity = useIdentityContext();

  return identity.user ? <Component {...rest} /> : null;
}

export default PrivateRoute;
```


This is a super simple and straight to the point component.  We are importing our Netlify Identity context and then using that, we are returning a component if a user is logged in and null if they are not.

So essentially this component serves as a wrapper for another component that we specify and only serves that component if a user is logged in.

If you scroll back up to our `account.js` page, you will see that the component we are wrapping here is called Profile.  So let’s create a new component file called `profile.js` and in it we will put:


``` javascript
import React from "react"
import { useIdentityContext } from 'react-netlify-identity-gotrue';

const Profile = () => {
  const identity = useIdentityContext();

  if (!identity.user) {
    return <div>Loading...</div>;
  }
  return(
    <>
      <section className="blog-wrapper" style={{textAlign: 'center'}}>
        <h2>Account Details</h2>
        <p>Email: {identity.user.email}</p>
      </section>
    </>
  )
}

export default Profile;
```


This component is the actual content that will be served to a logged in user browsing to `/account/profile/`.  Here we are using our Identity context to check if a user’s details are available, if not they are shown a simple loading message.  Then once the logged in user’s details have been loaded, the profile page simply displays their email address.  (which is the only information we have collected from them in this simple example)

And with that, we have a basic but fully functioning member sign up and login flow.  Let’s look at how we can add paid subscriptions to that next using Paddle and Fauna.


## Creating subscription plans using Paddle

Next we will look at how we can let our users manage subscription plans using their account profiles that we just created.  To manage our subscriptions, we will use a service called Paddle.

Essentially Paddle takes care of all of the billing and invoicing issues that come with selling a SaaS product and it has an easy-to-use dashboard as well as lots of API functionality that makes it very developer friendly.

For the purposes of our example, we will [create a new Paddle sandbox account here](https://sandbox-vendors.paddle.com/signup).

Once we have our account, we will create a new Subscription Plan by going to Catalog > Subscription Plans.  Create a new plan and give it whatever name and price you like - just take note of the Plan ID that is generated once you create the plan.

Now let’s get Paddle connected to our Gatsby build.  There is a helpful plugin called `gatsby-plugin-paddle` that injects Paddle’s script into your site automatically, but unfortunately it doesn’t support Paddle’s sandbox environment.  So instead of using the plugin, I have taken the code from this plugin, modified it slightly and we will use it directly in our project.

To do that, we will create a new file in our root folder called `gatsby-ssr.js` and inside it we will paste:


``` javascript
const React = require(`react`);

exports.onRenderBody = ({ setPostBodyComponents }) => {
	if (process.env.NODE_ENV === `production`) {
		return setPostBodyComponents([
			<script key="gatsby-plugin-paddle" src="https://cdn.paddle.com/paddle/paddle.js" />,
			<script
				key={`gatsby-plugin-paddle-vendor`}
				type="text/javascript"
				dangerouslySetInnerHTML={{
					__html: `Paddle.Setup({ vendor: ${process.env.PADDLE_ID} });`
				}}
			/>
		]);
	}
	else {
		return setPostBodyComponents([
			<script key="gatsby-plugin-paddle" src="https://cdn.paddle.com/paddle/paddle.js" />,
			<script
				key={`gatsby-plugin-paddle-vendor`}
				type="text/javascript"
				dangerouslySetInnerHTML={{
					__html: `Paddle.Environment.set('sandbox'); Paddle.Setup({ vendor: ${process.env.PADDLE_ID} });`
				}}
			/>
		]);
	}
};
```


We will also need to create a `.env.development` file in our root directory and put inside of that:


```
PADDLE_ID=YOUR_VENDOR_ID
PADDLE_PRODUCT_ID=YOUR_PADDLE_PRODUCT_ID
```


The `gatsby-ssr.js` file is hooking into Gatsby’s build process and every time a page is generated it injects Paddle’s script into the body of that page.  The Paddle script requires your Vendor ID which you can find in the Paddle dashboard under Developer Tools > Authentication.  We are storing that as an .env variable since it should be kept secret and not logged into your source control.

The original plugin only added the Paddle script to production builds, so I modified the code to set Paddle to sandbox mode if you are not running in production.  This way, you could create an `.env.production` file and put your Paddle production Vendor ID there and be able to simultaneously test with the sandbox while you are in dev mode and then deploy smoothly to production.

With that configuration out of the way, let’s look at how we can actually use Paddle in our site.  Let’s open up our `utils.js` file and add the following:


``` javascript
export const openPaddleCheckout = (user) => {

  let options = {product: process.env.PADDLE_PRODUCT_ID};
  if(user && user.email) {
    options.email = user.email;
  }
  let Paddle = window['Paddle'];
  if (Paddle) {
    Paddle.Checkout.open(options);
  }

}
```


Here we are creating a utility function that we can call from anywhere in our app, that will open up a new Paddle checkout session and serve it as an overlay to our site.  Using Paddle’s helper functions that were set up with our script injection previously, we just need to define some options and pass these into `Paddle.Checkout.open()`.

For the options, we are pulling the Paddle Production ID from our `.env.development` file and then we are checking if we have an email passed in with our user object comes from Netlify Identity.  If we have an email, this will be passed into our options which tells Paddle to pre-populate the email address so the user doesn’t have to type it in again.

Finally, we will check that the Paddle script is available and loaded using `window['Paddle']` before we actually call it.

Now let’s go back to our index.js file in order to use our Paddle checkout.  First, we will make some additions to the top half of the code:


``` javascript
import React, { useState, useEffect } from 'react';
import { Link, graphql } from "gatsby";
import { useIdentityContext } from 'react-netlify-identity-gotrue';

import Bio from "../components/bio";
import Layout from "../components/layout";
import Seo from "../components/seo";
import Button from "../components/button";
import Popup from "../components/popup";
import RegisterForm from "../components/registerForm";
import { openPaddleCheckout } from "../utils/utils";

const BlogIndex = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`;
  const posts = data.allMarkdownRemark.nodes;
  const identity = useIdentityContext();

  const[showPaddleCheckout, setShowPaddleCheckout] = useState(false);

  useEffect(() => {
    if (identity.user && showPaddleCheckout) {
      openPaddleCheckout(identity.user);
      setShowPaddleCheckout(false);
    }
  }, [identity.user, showPaddleCheckout])

…
```


Here we are importing our `openPaddleCheckout` function as well as React’s `useEffect`.  We will also create a React hook called `showPaddleCheckout.`

Next we will create a `useEffect` that checks if there is a logged in user and if our `showPaddleCheckout` hook is set to true.  If both of these are the case, then we will call our `openPaddleCheckout` function while passing in our user object from Netlify Identity.  Finally, we will call `setShowPaddleCheckout` and set it to false, so that we don’t initiate another checkout session if the page is refreshed.

Note that we have passed an array of dependencies to our `useEffect` that includes `identity.user` and `showPaddleCheckout`.  This means that if the value of either of these two dependencies changes the `useEffect` will be called again.

Now we just have to add a button where we can set the value of our `showPaddleCheckout` hook.  Let’s add it right beneath where we are showing all of our blog posts on the `index.js` page:


``` javascript
  return (
    <Layout location={location} title={siteTitle}>
      <Seo title="All posts" />
      <Bio />
      <ol style={{ listStyle: `none` }}>
        {posts.map(post => {
          const title = post.frontmatter.title || post.fields.slug

          return (
            <li key={post.fields.slug}>
              <article
                className="post-list-item"
                itemScope
                itemType="http://schema.org/Article"
              >
                <header>
                  <h2>
                    <Link to={post.fields.slug} itemProp="url">
                      <span itemProp="headline">{title}</span>
                    </Link>
                  </h2>
                  <small>{post.frontmatter.date}</small>
                </header>
                <section>
                  <p
                    dangerouslySetInnerHTML={{
                      __html: post.frontmatter.description || post.excerpt,
                    }}
                    itemProp="description"
                  />
                </section>
              </article>
            </li>
          )
        })}
      </ol>
      {
        identity.user &&
        <>
          <h1>Add Subscription</h1>
          <Button showPaddleCheckout={showPaddleCheckout} setShowPaddleCheckout={setShowPaddleCheckout} user={identity.user}>Subscribe</Button>
        </>
      }

    </Layout>
  )
```


After the list of posts, we are checking if a user is logged in then simply serving a button with some header text.


![Gatsby Blog with Membership](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yc6t7e875in6xsaccxys.png)


Notice though that we will need to update our `button.js` component to handle this new case of adding a subscription.  So we will modify our code to the following:


``` javascript
import React from 'react';

import { handleKeyDown } from '../utils/utils';

const Button = ({ children, showPopup, setShowPopup, showPaddleCheckout, setShowPaddleCheckout, user }) => {

  const handleLoginClick = () => {
    if (!user) {
      setShowPopup(!showPopup);
    }
    if (setShowPaddleCheckout && !showPaddleCheckout) {
      setShowPaddleCheckout(true);
    }
  }

  const handleLoginKeyDown = (e) => {
    if (!user) {
      handleKeyDown(e, showPopup, setShowPopup);
    }
    if (setShowPaddleCheckout && !showPaddleCheckout) {
      setShowPaddleCheckout(true);
    }
  }

  return (
    <button
      onClick={ (e) => handleLoginClick() }
      onKeyDown={ (e) => handleLoginKeyDown(e) }
       >{children}</button>

  )
}

export default Button;
```


We are now passing in `showPaddleCheckout`, `setShowPaddleCheckout` and our Netlify `user` as props.  In both `handleLoginClick` and `handleLoginKeyDown` we are first checking if we have a prop for `setShowPaddleCheckout `and that `showPaddleCheckout` is not already set to true.  Then we are calling `setShowPaddleCheckout(true)` which will then trigger our `useEffect` to serve our Paddle checkout.

![Paddle with Gatsby Blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59ox4o8dnog8i17pyjpw.png)


## Subscription management from member accounts using Fauna

Finally, let’s tie everything together using Fauna.  Credit to [Jason Lengstorf ](https://www.lengstorf.com/)and [his great article on connecting Stripe to Netlify using Fauna](https://www.netlify.com/blog/2020/07/13/manage-subscriptions-and-protect-content-with-stripe/#store-relationships-between-netlify-identity-users-and-stripe-customers) that formed the basis of a lot of the following code.

If you haven’t yet already, head over to [fauna.com](https://fauna.com/?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaPaddleGatsbyNetlify_GDickens ) and get signed up for a free account.  Then you will need to create a new database, give it whatever name you want.

Once you have the database created, we will need to define its schema.  I’m following Jason’s advice here and storing our schema in a new `db` folder in our project’s root directory.  Call this file `schema.gql` and paste in the following:


```
type User {
  netlifyID: ID!
  paddleSubID: ID!
}

type Query {
  getUserByNetlifyID(netlifyID: ID!): User!
}
```


This is telling our Fauna instance that our database will have a data type of `User` that will have two ID types: ` netlifyID` and `paddleSubID`.  We are also defining one query that will expect a `netlifyID` and will return the `User` object.

Now we will upload this into Fauna by going to GraphQL > Import Schema.

Next we need to generate a Server Key from Fauna that we can use to connect from our Gatsby site.  On the left-hand menu go to Security and then New Key.  Now we will save this key in our `.env.development` file:


```
PADDLE_ID=YOUR_VENDOR_ID
PADDLE_PRODUCT_ID=YOUR_PADDLE_PRODUCT_ID
FAUNA_SERVER_KEY=YOUR_SERVER_KEY
```


As a last configuration step, we will install the `node-fetch` package to our project so that we can use fetch in our Netlify Serverless functions.  We will need to use Serverless functions to connect to our Fauna instance, since we don’t want to expose our secret key in our client-side code.

So we will run:


```
npm install --save node-fetch
```


Now with that set up, we will start writing our Netlify serverless functions.  Let’s start with a new file in `functions/utils/fauna.js`:


``` javascript
const fetch = require('node-fetch');

exports.faunaFetch = async ({ query, variables }) => {
  return await fetch('https://graphql.fauna.com/graphql', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.FAUNA_SERVER_KEY}`,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  })
    .then((res) => res.json())
    .catch((err) => console.error(JSON.stringify(err, null, 2)));
};
```


Credit to Jason again, as this was his creation in his tutorial.  Here we are just creating a boilerplate fetch function that we will be able to reuse in order to access our Fauna instance.  Note that the function expects query and variables objects and then converts these into JSON in the body of the request.  Also note we are pulling our Fauna Server Key from our `.env.development` file.

Now let’s use our `faunaFetch` utility function in a new file `functions/addSub.js`:


``` javascript
const { faunaFetch } = require('./utils/fauna');

exports.handler = async (event) => {
  // Only allow POST
  if (event.httpMethod !== 'POST') {
    return {
      statusCode: 200,
      body: JSON.stringify('Method not allowed')};
  }

  const { netlifyID, paddleSubID } = JSON.parse(event.body);

  // link the Netlify and Paddle Sub IDs in Fauna
  await faunaFetch({
    query: `
      mutation ($netlifyID: ID!, $paddleSubID: ID!) {
        createUser(data: { netlifyID: $netlifyID, paddleSubID: $paddleSubID }) {
          netlifyID
          paddleSubID
        }
      }
    `,
    variables: {
      netlifyID: netlifyID,
      paddleSubID: paddleSubID,
    },
  });

  return {
    statusCode: 200,
    body: JSON.stringify('Sub Added')
  };
};
```


In order to create a new record in our Fauna we will specify our mutation query that expects both a Netlify User ID and a Paddle Subscription ID.  We will then be storing both of those in our Fauna database so we can link one to the other.  That way, we can fetch our users by their subscription ID and vice versa.

Now we need to create a function on the client-side to call our serverless function to add a new user.  Let’s go back to our `src/utils/utils.js` file and add the following:


``` javascript
const addPaddleSubscription = ( netlifyID, paddleSubID ) => {
  async function postData(url) {
     const response = await fetch(url, {
       method: 'POST',
       mode: 'cors',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({netlifyID: netlifyID, paddleSubID: paddleSubID})
     });
     return response.json();
   }
   postData('/api/addSub/')
     .then(data => {
       console.log("Success: ", data);
     })
     .catch((error) => {
        console.log('Error: ', JSON.stringify(error));
      });
  }
```


Here we are creating an async function that will call our serverless function at `/api/addSub/` passing our Netlify ID and Paddle Subscription ID’s into the body.

Now we will just need to modify our `openPaddleCheckout` function in our same file:


``` javascript
export const openPaddleCheckout = (user) => {

  const checkoutComplete = (data) => {
    var checkoutId = data.checkout.id;

    Paddle.Order.details(checkoutId, function(data) {
      addPaddleSubscription(user.id, data.order.subscription_id)
    });
  }
  let options = {product: process.env.PADDLE_PRODUCT_ID, successCallback: checkoutComplete};
  if(user && user.email) {
    options.email = user.email;
  }
  let Paddle = window['Paddle'];
  if (Paddle) {
    Paddle.Checkout.open(options);
  }

}
```


Here we are adding an additional option to our `Paddle.Checkout.open` called `successCallback `which runs after a payment is made successfully.

In our `checkoutComplete` function we get the checkout ID that is passed to the callback as `data.checkout.id`.  Then with this ID, we can get further details from Paddle by calling `Paddle.Order.details(checkoutId)`.  Finally, this function also has a callback function where we will call `addPaddleSubscription()` while passing in our Netlify User ID and the Paddle Subscription ID that we just accessed from the details via `data.order.subscription_id`.

Now that we’ve linked our Netlify ID’s with Paddle subscriptions using Fauna, let’s build a way to display a Paddle subscription in a user’s profile.  To do this, we will need to create another serverless function and accompanying client-side API call to get a subscription ID by Netlify user ID.

Let’s start with our serverless function we will create in a new file at `functions/getSub.js`:


``` javascript
const { faunaFetch } = require('./utils/fauna');

exports.handler = async (event) => {
  // Only allow POST
  if (event.httpMethod !== 'POST') {
    return {
      statusCode: 200,
      body: JSON.stringify('Method not allowed')};
  }

  try {
    const { netlifyID } = JSON.parse(event.body);

    const result = await faunaFetch({
      query: `
        query ($netlifyID: ID!) {
          getUserByNetlifyID(netlifyID: $netlifyID) {
            netlifyID
            paddleSubID
            _id
          }
        }
      `,
      variables: {
        netlifyID: netlifyID,
      },
    });

    return {
      statusCode: 200,
      body: JSON.stringify({subs: true, data: result.data.getUserByNetlifyID}),
    };

  }
  catch (err) {
    console.log(err); // output to netlify function log
    return {
      statusCode: 200,
      body: JSON.stringify({subs: false, data: {} }),
    };
  }
};
```


Most of this function is the same as `addSub`, we are just passing in different data and using a different Fauna query.  Here we are parsing the Netlify ID via the body of our POST call and then using that in a Fauna query (rather than a mutation).  We are calling the query that we previously defined in our schema (`getUserByNetlifyID`) and we are asking that query to return the `paddleSubID` as well as `_id` which is the Fauna ID for the record in the database. (we will use this a bit later when we want to delete records)

Note the structure of the data that we are returning.  We are returning an object that has a key “subs” that will either be true or false, depending on if any subs currently exist for that user ID and then we are returning the actual data under the key “data.”

Now that we have our query and serverless function ready, let’s create our POST request in `src/utils/utils.js` by adding the following:


``` javascript
export const  getPaddleSubscription = async ( netlifyID ) => {
  async function postData(url) {
     const response = await fetch(url, {
       method: 'POST',
       mode: 'cors',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({netlifyID: netlifyID})
     });
     return response.json();
   }
  return await postData('/api/getSub/');
}
```


This is a very straight-forward function that simply calls our new serverless function while passing in the Netlify ID and returns the response, which will be the Netlify ID, Paddle Subscription ID and Fauna ID.

Now let’s use this data in our user profile component.  We will replace our `src/components/profile.js` with the following:


``` javascript
import React, { useState, useEffect } from 'react';
import { useIdentityContext } from 'react-netlify-identity-gotrue';
import { getPaddleSubscription } from '../utils/utils';

const Profile = () => {
  const identity = useIdentityContext();

  const [loadingSubscription, setLoadingSubscription] = useState(true);
  const [subscription, setSubscription] = useState(null);

  useEffect(() => {
    if (loadingSubscription) {
      getPaddleSubscription(identity.user.id)
        .then(newSub => {
          if (newSub.subs) {
            setSubscription(newSub.data);
          }
          setLoadingSubscription(false);
        })
        .catch(err => {
          setLoadingSubscription(false);
        })
    }
  }, [loadingSubscription, setSubscription, setLoadingSubscription, subscription, identity.user.id])

  if (!identity.user) {
    return <div>Loading...</div>;
  }
  return(
    <>
      <section className="blog-wrapper" style={{textAlign: 'center'}}>
        <h2>Account Details</h2>
        <p>Email: {identity.user.email}</p>
        <h3>Subscriptions</h3>
        {loadingSubscription && <p>Loading Subscriptions</p>}
        {!loadingSubscription && subscription !== null &&
          <p>Test Subscription: {subscription.paddleSubID} </p>
        }
        {!loadingSubscription && subscription === null &&
          <p>No current subscriptions</p>
        }

      </section>
    </>
  )
}

export default Profile;
```


Here we have two React Hooks that are controlling the action:  `loadingSubscription` and `subscription`.  By default, `loadingSubscription` is set to `true` which will cause our `useEffect` to be triggered when the component is initially loaded.  In the `useEffect`, we then call our `getPaddleSubscription()` function passing in our user ID from our Netlify Identity context.  Once that is returned, we will check if any subscription was found by checking if `subs` is `true`, then we set the subscription ID that is returned using `setSubscription()`.  Finally, whether a subscription was found or not, we will set `loadingSubscription` to `false`.

Note that we have the following as dependencies in our `useEffect`:  `subscription, setSubscription, loadingSubscription, setLoadingSubscription, identity.user.id`.  So if any of these change our `useEffect` will run again, although it will not actually fetch new data unless `loadingSubscription` is set to `true`.

Then to display our data, we are using a few conditionals in our HTML.  If `loadingSubscription` is `true`, we are simply displaying a message that says “Loading Subscriptions.”  Then once `loadingSubscription` is complete and set to `false`, we have two options:

* If a subscription is found, it will be displayed with it’s Paddle ID.  (You could do a lot more here by displaying other subscription data to your user, but for the purpose of this example we will just show the ID)
* If no subscription is found, it simply says “No current subscriptions”

![Gatsby Manage Subscriptions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/55ogfvca2yyi4j3v0hpd.png)

Finally, let’s take things one step further and let our user’s cancel their subscription from their account profile.

First we will need an API key from Paddle which you can find in the Paddle Dashboard under Developer Tools > Authentication, then you can just use the Default API Key and press Reveal Auth Code.  Paste this code into your `.env.development` file:


```
PADDLE_ID=YOUR_VENDOR_ID
PADDLE_PRODUCT_ID=YOUR_PADDLE_PRODUCT_ID
FAUNA_SERVER_KEY=YOUR_SERVER_KEY
PADDLE_AUTH_CODE=YOUR_PADDLE_API_KEY
```


Now let’s create a new serverless utility function in functions/utils called `paddleCancel.js`:


``` javascript
const fetch = require('node-fetch');

exports.paddleCancel = async (subID) => {
  return await fetch('https://sandbox-vendors.paddle.com/api/2.0/subscription/users_cancel', {
    method: 'POST',
    body: new URLSearchParams({
        'vendor_id': process.env.PADDLE_ID,
        'vendor_auth_code': process.env.PADDLE_AUTH_CODE,
        'subscription_id': subID
    })
  })
    .then((res) => res.json())
    .catch((err) => console.error(JSON.stringify(err, null, 2)));
};
```


Here we are calling the Paddle API ([more details here](https://developer.paddle.com/api-reference/subscription-api/users/canceluser)) using fetch and passing in form encoded data in the body with three items:  `vendor_id` and `vendor_auth_code` simply come from our `.env` file while `subscription_id` will be provided as an argument to our function.

Now let’s use this utility function in a new serverless function `functions/deleteSub.js`:


``` javascript
const { faunaFetch } = require('./utils/fauna');
const { paddleCancel } = require('./utils/paddleCancel');

exports.handler = async (event) => {
  // Only allow POST
  if (event.httpMethod !== 'POST') {
    return {
      statusCode: 200,
      body: JSON.stringify('Method not allowed')};
  }

  const { netlifyID } = JSON.parse(event.body);

  const result = await faunaFetch({
    query: `
      query ($netlifyID: ID!) {
        getUserByNetlifyID(netlifyID: $netlifyID) {
          netlifyID
          paddleSubID
          _id
        }
      }
    `,
    variables: {
      netlifyID: netlifyID,
    },
  });

  const paddleResult = await paddleCancel(result.data.getUserByNetlifyID.paddleSubID);
  console.log(paddleResult);

  await faunaFetch({
    query: `
      mutation ($faunaID: ID!) {
        deleteUser(id: $faunaID) {
          netlifyID
          paddleSubID
        }
      }
    `,
    variables: {
      faunaID: result.data.getUserByNetlifyID._id,
    },
  });

  return {
    statusCode: 200,
    body: JSON.stringify('Sub Deleted'),
  };
};
```


In this function we have three important things happening one after the other:



* First, we are querying Fauna by Netlify ID and returning the Paddle Subscription ID as well as the Fauna ID.  (same as we did in `getSub.js`)
* Then we are using that Paddle Subscription ID to call our `paddleCancel` function.  (we are just logging the response from Paddle in the example)
* After that, we are calling Fauna with a new mutation that deletes our User data item

Note, this is why we needed to return our Fauna `_id` before, since it is required in order to delete the item.

As with all of our serverless functions, we need to add an accompanying client-side API call, so let’s add that to our `src/utils/utils.js` file:


``` javascript
export const cancelPaddleSubscription = ( netlifyID ) => {
  async function postData(url) {
     const response = await fetch(url, {
       method: 'POST',
       mode: 'cors',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({netlifyID: netlifyID })
     });
     return response.json();
   }
   postData('/api/deleteSub/')
     .then(data => {
       console.log("Success: ", data);
     })
     .catch((error) => {
        console.log('Error: ', JSON.stringify(error));
      });
  }
```


Again, very straightforward - we are just passing our Netlify ID into the body and calling `/api/deleteSub/` with a POST request.

Finally to get all this online we just need to update our UI, so let’s go back to our `src/components/profile.js` and make a couple of quick changes:


``` javascript
import { getPaddleSubscription, cancelPaddleSubscription } from '../utils/utils';
```


…


``` javascript
  return(
    <>
      <section className="blog-wrapper" style={{textAlign: 'center'}}>
        <h2>Account Details</h2>
        <p>Email: {identity.user.email}</p>
        <h3>Subscriptions</h3>
        {loadingSubscription && <p>Loading Subscriptions</p>}
        {!loadingSubscription && subscription !== null &&
          <p><span>Test Subscription: {subscription.paddleSubID} </span><button onClick={e => {cancelPaddleSubscription(identity.user.id); setSubscription(null); }}>Cancel</button></p>
        }
        {!loadingSubscription && subscription === null &&
          <p>No current subscriptions</p>
        }

      </section>
    </>
  )
```


We are adding our new `cancelPaddleSubscription` to our imports and then we are using it in the HTML that is served when a user has a subscription.  Then we have put a simple button inside a span next to the Paddle Subscription ID which when clicked will call `cancelPaddleSubscription` with the Netlify User ID as an argument.  It will also set `subscription` back to `null` so the subscription will disappear as soon as the button is clicked.

![Gatsby Account Management](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmbuqdx4cioci33thcbk.png)

And that’s it!  We’ve now got a fully functioning account management service that let’s our users subscribe and cancel subscriptions on their own.


## Summary

In this post, you learned how to:

* Set up a basic Gatsby site
* Configure and deploy it to Netlify
* Manage memberships using Netlify Identity
* Add login and signup modals using React hooks
* Add private member areas using Gatsby private routes
* Create subscription plans using Paddle
* Manage subscriptions from users’ accounts using Fauna

This post gives you the basic flow of how you can incorporate memberships and subscriptions into your static site and how a simple and flexible database like Fauna can help you pull it all together.

You can get even more ideas of what you can do with them together by checking out more of my work at [Epilocal](https://www.epilocal.com/) where I am building technology for local news and other small online publishers using this same tech stack to manage my memberships and SaaS subscriptions.

_Written in connection with the [Write with Fauna Program](https://fauna.com/blog/write-with-fauna?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FaunaPaddleGatsbyNetlify_GDickens)._