Working with Apollo Client and Clerk session tokens

Grafbase supports using OpenID Connect as your authentication and authorization strategy. You can configure your schema with rules based on your signed-in and user group statuses. Grafbase also enables you to restrict allowed operations per rule.

Clerk is a popular choice when it comes to connecting an OIDC provider with Grafbase. In this guide we'll explore using some of the the @clerk/nextjs package functions.


Clerk uses short-lived tokens for added security, and requires you to call getToken() before every request to get a new token (if required) to pass with requests.

You should read this guide on how to configure Clerk as your identity provider.

This guide assumes you have an existing project with Clerk and Next.js setup. You can use the nextjs-clerk example to follow along if you haven't already got a project.

We'll configure the barebones of Apollo Client to get data flowing. We won't be making any optimizations for SSR, or caching in this guide.

npm install @apollo/client graphql

Then inside of pages/_app.tsx we'll configure our HttpLink and pass it our Grafbase API URL:

import { HttpLink } from '@apollo/client'

const httpLink = new HttpLink({
  uri: 'YOUR_GRAFBASE_API_URL'
})

Now create a new <ApolloProviderWrapper /> functional component so we can invoke setContext on our Apollo Client HttpLink. Inside of this function we'll create and memoize a new ApolloClient instance and call it client:

import type { PropsWithChildren } from 'react'
import { useMemo } from 'react'
import {
  ApolloClient,
  ApolloProvider,
  InMemoryCache,
  from
} from '@apollo/client'

export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
  const client = useMemo(() => {
    return new ApolloClient({
      link: from([httpLink]),
      cache: new InMemoryCache()
    })
  }, [])

  return <ApolloProvider client={client}>{children}</ApolloProvider>
}

You'll also notice above that we're passing an array to from for our ApolloClient link. We need to do this so we can create a single ApolloLink that is built up of the context we'll pass it next.

We'll next import setContext from @apollo/client. With this we can create authMiddleware that we then pass to from. The last link httpLink is what excutes the GraphQL request to Grafbase.

import { setContext } from '@apollo/client/link/context'

export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
  const client = useMemo(() => {
    const authMiddleware = setContext(async (operation, { headers }) => {
      return {
        headers: {
          ...headers,
          authorization: `Bearer TOKEN_HERE`
        }
      }
    })

    return new ApolloClient({
      link: from([authMiddleware, httpLink]),
      cache: new InMemoryCache()
    })
  }, [])

  return <ApolloProvider client={client}>{children}</ApolloProvider>
}

Finally we'll return from inside of authMiddleware our new request headers with our token from Clerk! You'll need to pass it a "template" so it can create the correct JWT that includes any groups you belong to for authorization purposes.

Clerk manages caching and refreshing the state of getToken. We'll want to memoize getToken to prevent unncessary HTTP requests to Clerk when the token is still valid.

import { useAuth } from '@clerk/nextjs'

export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
  const { getToken } = useAuth()

  const client = useMemo(() => {
    const authMiddleware = setContext(async (operation, { headers }) => {
      const token = await getToken({ template: 'grafbase' })

      return {
        headers: {
          ...headers,
          authorization: `Bearer ${token}`
        }
      }
    })

    return new ApolloClient({
      link: from([authMiddleware, httpLink]),
      cache: new InMemoryCache()
    })
  }, [getToken])

  return <ApolloProvider client={client}>{children}</ApolloProvider>
}

Don't forget to add getToken to the dependencies of useMemo.

Now where you would normally use <ApolloProvider /> to wrap your application... Just use <ApolloProviderWrapper /> and it will take care of passing the correct token from Clerk to Grafbase!

Let's now look at what this looks like with a custom _app.tsx component for a typical Next.js application.

We'll make sure the following scenarios are covered:

  • Public pages are accessible without logging in, and have GraphQL context
  • Signed in users can access protected pages
  • Signed out users that attempt to access protected pages are redirected to sign-in
import {
  ClerkLoaded,
  ClerkProvider,
  RedirectToSignIn,
  SignedIn,
  SignedOut
} from '@clerk/nextjs'

const publicPages = [
  '/',
  '/about-us',
  '/privacy-policy',
  '/sign-in/[[...index]]',
  '/sign-up/[[...index]]'
]

const MyApp = ({ Component, pageProps, router }) => {
  return (
    <ClerkProvider {...pageProps}>
      <ClerkLoaded>
        <ApolloProviderWrapper>
          {publicPages.includes(router.pathname) ? (
            <Component {...pageProps} />
          ) : (
            <Fragment>
              <SignedIn>
                <Component {...pageProps} />
              </SignedIn>
              <SignedOut>
                <RedirectToSignIn />
              </SignedOut>
            </Fragment>
          )}
        </ApolloProviderWrapper>
      </ClerkLoaded>
    </ClerkProvider>
  )
}

This next step will depend on your schema, and rules. For the purposes of this next step, we'll use the following schema and rules:

schema
  @auth(
    providers: [{ type: oidc, issuer: "{{ env.CLERK_ISSUER_URL }}" }]
    rules: [
      { allow: private, operations: ["read"] }
      { allow: groups, groups: ["admin"], operations: ["delete"] }
    ]
  ) {
  query: Query
}

type Post @model {
  id: ID!
  title: String!
  comments: [Comment!]!
}

type Comment @model {
  id: ID!
  message: String!
  post: Post!
}

The above schema allows any signed-in user to read data, this includes fetching a collection, by id or unique field.

We'll now import gql and useQuery from @apollo/client to execute our first GraphQL operation. We'll write a query in this example, but this could also be a mutation.

import { gql, useQuery } from '@apollo/client'

const QUERY = gql`
  {
    postCollection(first: 10) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
`

const MyPage = () => {
  const { data, loading } = useQuery(QUERY)

  if (loading) return <p>Loading!</p>

  return <pre>{JSON.stringify(data, null, 2)}</pre>
}

export default MyPage

You'll notice the request to Grafbase is unauthorized if you aren't signed in. However, if you sign in to your application, the query to read data will be allowed!

It doesn't matter if you have any data in your project as the request will be allowed or denied based on the token rules.

Get started with Grafbase, Clerk and Apollo Client

Get early access to the Grafbase beta.