post picture
Open Source Stripe GraphQL API
Published: 28 September 2022

Today we’re launching an open source Stripe GraphQL API that makes it possible to query data from your database and Stripe account in a single GraphQL query. You can start using it today with our official npm package, @nhost/stripe-graphql-js, and get a ready-to-use Stripe GraphQL API server.

Stripe GrahQL API

Stripe GrahQL API

Note: @nhost/stripe-graphql-js creates an unofficial open-source Stripe GraphQL API. The source code is available on GitHub.

How does it work?

The generated Stripe GraphQL API can be hosted using a Serverless Function and added as a Hasura Remote Schema. After that, a GraphQL relationship is created between your data in Postgres and Stripe using Hasura Data Federation.

We’ll explain how it works in more detail. But first…

Why did we build this?

Developers are using Nhost to build SaaS applications and Stripe to handle payments.

The problem is that some customer data lives in the Postgres database while other data is in Stripe. One of the selling points of GraphQL is being able to fetch all data required in a single request regardless of where that data is coming from. This is why having to manually combine data from Postgres and Stripe is not ideal.

We faced this first hand while building our dashboard - every workspace in Nhost is a Stripe customer. In the workspace view, we want to show data from both our database and Stripe accounts.

It would be great if we could make a single GraphQL query to show this information.

Well, now we can!

Workspace Example

Workspace Example

With GraphQL, it’s possible to query data from any data source. Not only a database.

Now, with the package being introduced, it’s possible to get data from both the Postgres database and Stripe using a single GraphQL query.

This is called Data Federation and is a powerful concept to unify data through a single GraphQL API.

Image from https://hasura.io/blog/announcing-hasura-graphql-v1-3-0/

Image from https://hasura.io/blog/announcing-hasura-graphql-v1-3-0/

Get Started

This is how you get started.

Serverless Function

First, create a Serverless Function to host and scale the Stripe GraphQL API. Here’s a minimal example of the Stripe GraphQL server:

functions/graphql/stripe.ts

import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js';

const server = createStripeGraphQLServer();

export default server;

That’s it. The GraphQL API is ready to be added as a Remote Schema.

Remote Schema in Hasura

You can now head to Remote Schemas in the Hasura Console and add the Stripe GraphQL API previously created. This way, the Stripe GraphQL API will be available in your main GraphQL API. This is called schema stitching.

The Stripe GraphQL API will available at:

{{NHOST_BACKEND_URL}}/v1/functions/graphql/stripe

We're using here a System Environment Variable, NHOST_BACKEND_URL, to make sure the URL works both locally and in production.

Hasura Data Federation Relationship

Finally, we can now create a relationship from an existing table in the database to Stripe.

Usually, you would have a stripe_customer_id column in some table and from that column, create a relationship to a customer in Stripe.

You now have the Stripe GraphQL API set up and configured. It’s added as a Remote Schema in Hasura, and there is a relationship between the stripe_customer_id column and the customer id in Stripe.

Almost like magic, it’s now possible to make GraphQL queries to get data from both the database and Stripe.

GraphQL Objects

The following GraphQL object types are available:

At the moment, not all fields are available on all objects, but most are. We’ll continue the work to make the Stripe GraphQL API as complete as possible.

If you want to help us, the code is open source and available on GitHub - we would be more than happy to accept contributions.

Examples

Here are some examples of GraphQL queries and mutations to show the power of the Stripe GraphQL API.

Example: Get all invoices for a customer:

query {
  stripe {
    customer(id: "cus_xxx") {
      id
      invoices {
        data {
          id
          paid
          amountPaid
          amountDue
          hostedInvoiceUrl
        }
      }
    }
  }
}

Example: Get all payment methods for a customer:

query {
  stripe {
    customer(id: "cus_xxx") {
      id
      paymentMethods {
        data {
          id
          created
          card {
            brand
            expMonth
            expYear
            last4
          }
        }
      }
    }
  }
}

Example: Get all subscriptions and the subscription’s items (product name and cost) for a customer:

{
  stripe {
    customer(id: "cus_xxx") {
      id
      subscriptions {
        data {
          id
          status
          items {
            data {
              id
              quantity
              price {
                product {
                  id
                  name
                  defaultPrice {
                    id
                    currency
                    unitAmount
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Example: Get a billing portal session URL:

mutation {
  stripe {
    createBillingPortalSession(customer: "cus_xxx") {
      id
      url
    }
  }
}

Permissions

Permissions are managed in two ways:

  1. x-hasura-admin-secret header for full access.

  2. isAllowed function for granular permissions.

The isAllowed function checks if a current operation is allowed, like interacting with a customer in Stripe.

Here’s an example that uses the isAllowed function and does the following:

  1. If the request is made with a valid admin secret, isAdmin is true, we allow all operations.

  2. Get the user’s id.

  3. Get user information from the database.

  4. Loop over the user’s workspaces to see if the user belongs to a workspace with the same stripe customer id as the stripeCustomerId for the operation.

import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'

const isAllowed = (stripeCustomerId: string, context: Context) => {
const { isAdmin, userClaims } = context

  // allow requests if it has a valid `x-hasura-admin-secret`
  if (isAdmin) {
    return true
  }

  // get user id
  const userId = userClaims['x-hasura-user-id']

  // check if user is signed in
  if (!userId) {
    return false;
  }

  // get more user information from the database
  const { user } = await gqlSDK.getUser({
    id: userId,
  });

  if (!user) {
    return false;
  }

  // check if the user is part of a workspace with the `stripeCustomerId`
  return user.workspaceMembers
    .some((workspaceMember) => {
      return workspaceMember.workspace.stripeCustomerId === stripeCustomerId;
    });
}

const server = createStripeGraphQLServer({
  isAllowed
})

export default server

As you see, the isAllowed function allows you to create a custom function to manage permissions in your Stripe GraphQL API.

How we built the Stripe GraphQL API

This Stripe GraphQL API would not have been possible without some excellent open-source libraries.

The Stripe GraphQL API schema is built using Pothos, a GraphQL Schema builder for TypeScript. Using the type definitions from Stripe it was easy to create the Stripe GraphQL schema.

Btw, Nhost is a proud sponsor of Pothos.

We also used the awesome GraphQL Yoga, a fully-featured GraphQL Server built-in TypeScript for the actual GraphQL Server. The core of GraphQL Yoga implements W3C Fetch API and can run on any JS environment - perfect for Serverless Functions.

GraphQL Yoga is maintained by The Guild, a team that does fantastic work for the GraphQL community.

These two tools were terrific to work with and made it easy to create a custom GraphQL API like this.

Permissions were possible because of:

  • Context to read the request headers and write a custom context for each GraphQL requests

  • Hasura Remote Schema that forwards all headers from the client. Specifically the Authorization header.

  • System environment variables make the NHOST_JWT_SECRET available in all Serverless Functions to verify the access token from the Authorization header request header.

This way, it’s possible to identify and verify the user making the request and write custom code to manage permissions.

Alternatives

What would developers have to do to solve this issue if it wasn’t for this new Stripe GraphQL API?

There are two alternatives:

  1. Webhook (duplicated data)

  2. Direct Access using REST

Let’s go a bit deeper into both of these alternatives to understand their drawbacks.

Webhook (duplicated data)

It’s possible to set up a webhook and have Stripe send a webhook if anything changes at Stripe. For E.g. if a new customer is created, a subscription is deleted, or a payment method is updated.

Once anything changes in Stripe, you must receive that event via a webhook and store the information in your database. Effectively duplicating the data in Stripe.

Duplicating data is generally a bad idea and leads to synchronization problems.

  • What if your webhook is down?

  • How do you get the initial set of your Stripe data?

Also, if you sync Stripe’s data to your database now, you’re responsible for data format, backups, scaling, etc. Things that Stripe is probably handling better.

Direct Access using REST

One alternative is to get the data from Stripe using Stripe’s REST API whenever necessary. The problem is that if your app uses GraphQL, you now have two ways of interacting with your data.

With REST, you need a separate client, and it’s impossible to create relationships between your data in GraphQL and Stripe.

Here’s a short example of how you would have to combine GraphQL and Stripe REST:

const GET_CUSTOMER = `
query {
  customer {
    id
    name
    stripeCustomerId
  }
}`;

const [stripeData, setStripeData] = useState();
const { data, loading  } = useQuery(GET_CUSTOMER);

useEffect(() => {
  const getStripeData = async () => {
    if (!loading && data.customer) {
      // Note: Custom endpoint to manage data access and permissions.
      // This endpoint is not included in this example.
      const invoices = await httpClient.call(`/api/stripe/invoices?stripe-customer-id=data.customer.stripeCustomerId`);
    }
  }

  getStripeData();
);

if (loading || !stripeData) {
  return <div>Loading...</div>
}

const { customer } = data;

return (
  <div>
    <div>{customer.name}'s invoices</div>
    <ul>
      {stripeData.invoices.map(invoice => {
        return (
          <li>{invoice.id} - <a href={invoice.hosted_invoice_url}>Link</a></li>
        )
      )}
    </ul>
  </div>
);

Compare the example above with the new Stripe GraphQL API with Nhost:

const GET_CUSTOMER = `
query {
  customer {
    id
    name
    stripeCustomer {
      invoices {
        data {
          id
          created
          paid
          currency
          amountDue
          amountPaid
        }
      }
    }
  }
}`;

const { data, loading  } = useQuery(GET_CUSTOMER);

if (loading) {
  return <div>Loading...</div>
}

const { customer } = data;

return (
  <div>
    <div>{customer.name}'s invoices</div>
    <ul>
      {customer.stipeCustomer.invoices.data.map(invoice => {
        return (
          <li>{invoice.id} - <a href={invoice.hosted_invoice_url}>Link</a></li>
        )
      )}
    </ul>
  </div>
);

What’s next

The @nhost/stripe-graphql-js package makes it easier for developers building SaaS applications to integrate Stripe and payments into their applications.

We’ll continue to make the Stripe GraphQL API package as complete as possible. The code is open source and available on GitHub, and it’s easy to contribute. The Stripe API is extensive, so there is a lot of work still left to do.

We’ll also work on making it easier to build custom GraphQL APIs for your application with Serverless Functions and Hasura Remote Schema. Something that we’ve only scratched the surface off.

PS. Support our Open Source work

Star us on GitHub ⭐

We use cookies to provide our services and for analytics and marketing. By continuing to browse our website, you agree to our use of cookies.
To find out more about our use of cookies, please see our Privacy Policy and Cookies Policy.