Open Source Stripe GraphQL API
28 September 2022Today 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
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
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/
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
_10import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'_10_10const server = createStripeGraphQLServer()_10_10export 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:
_10{{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:
_16query {_16 stripe {_16 customer(id: "cus_xxx") {_16 id_16 invoices {_16 data {_16 id_16 paid_16 amountPaid_16 amountDue_16 hostedInvoiceUrl_16 }_16 }_16 }_16 }_16}
Example: Get all payment methods for a customer:
_19query {_19 stripe {_19 customer(id: "cus_xxx") {_19 id_19 paymentMethods {_19 data {_19 id_19 created_19 card {_19 brand_19 expMonth_19 expYear_19 last4_19 }_19 }_19 }_19 }_19 }_19}
Example: Get all subscriptions and the subscription's items (product name and cost) for a customer:
_30{_30 stripe {_30 customer(id: "cus_xxx") {_30 id_30 subscriptions {_30 data {_30 id_30 status_30 items {_30 data {_30 id_30 quantity_30 price {_30 product {_30 id_30 name_30 defaultPrice {_30 id_30 currency_30 unitAmount_30 }_30 }_30 }_30 }_30 }_30 }_30 }_30 }_30 }_30}
Example: Get a billing portal session URL:
_10mutation {_10 stripe {_10 createBillingPortalSession(customer: "cus_xxx") {_10 id_10 url_10 }_10 }_10}
Permissions
Permissions are managed in two ways:
x-hasura-admin-secret
header for full access.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:
- If the request is made with a valid admin secret,
isAdmin
istrue
, we allow all operations. - Get the user's id.
- Get user information from the database.
- 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.
_39import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'_39_39const isAllowed = (stripeCustomerId: string, context: Context) => {_39const { isAdmin, userClaims } = context_39_39 // allow requests if it has a valid `x-hasura-admin-secret`_39 if (isAdmin) {_39 return true_39 }_39_39 // get user id_39 const userId = userClaims['x-hasura-user-id']_39_39 // check if user is signed in_39 if (!userId) {_39 return false;_39 }_39_39 // get more user information from the database_39 const { user } = await gqlSDK.getUser({_39 id: userId,_39 });_39_39 if (!user) {_39 return false;_39 }_39_39 // check if the user is part of a workspace with the `stripeCustomerId`_39 return user.workspaceMembers_39 .some((workspaceMember) => {_39 return workspaceMember.workspace.stripeCustomerId === stripeCustomerId;_39 });_39}_39_39const server = createStripeGraphQLServer({_39 isAllowed_39})_39_39export 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 theAuthorization
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:
- Webhook (duplicated data)
- 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:
_42const GET_CUSTOMER = `_42query {_42 customer {_42 id_42 name_42 stripeCustomerId_42 }_42}`;_42_42const [stripeData, setStripeData] = useState();_42const { data, loading } = useQuery(GET_CUSTOMER);_42_42useEffect(() => {_42 const getStripeData = async () => {_42 if (!loading && data.customer) {_42 // Note: Custom endpoint to manage data access and permissions._42 // This endpoint is not included in this example._42 const invoices = await httpClient.call(`/api/stripe/invoices?stripe-customer-id=data.customer.stripeCustomerId`);_42 }_42 }_42_42 getStripeData();_42);_42_42if (loading || !stripeData) {_42 return <div>Loading...</div>_42}_42_42const { customer } = data;_42_42return (_42 <div>_42 <div>{customer.name}'s invoices</div>_42 <ul>_42 {stripeData.invoices.map(invoice => {_42 return (_42 <li>{invoice.id} - <a href={invoice.hosted_invoice_url}>Link</a></li>_42 )_42 )}_42 </ul>_42 </div>_42);
Compare the example above with the new Stripe GraphQL API with Nhost:
_40const GET_CUSTOMER = `_40query {_40 customer {_40 id_40 name_40 stripeCustomer {_40 invoices {_40 data {_40 id_40 created_40 paid_40 currency_40 amountDue_40 amountPaid_40 }_40 }_40 }_40 }_40}`;_40_40const { data, loading } = useQuery(GET_CUSTOMER);_40_40if (loading) {_40 return <div>Loading...</div>_40}_40_40const { customer } = data;_40_40return (_40 <div>_40 <div>{customer.name}'s invoices</div>_40 <ul>_40 {customer.stipeCustomer.invoices.data.map(invoice => {_40 return (_40 <li>{invoice.id} - <a href={invoice.hosted_invoice_url}>Link</a></li>_40 )_40 )}_40 </ul>_40 </div>_40);
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.