Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Use Supabase with GraphQL

Supabase provides a built-in GraphQL API powered by the pg_graphql extension. Enable it in the SQL Editor, then query your database tables through the GraphQL endpoint at /graphql/v1 using standard GraphQL syntax. The API auto-generates types, queries, and mutations from your database schema, respects RLS policies, and works with any GraphQL client including Apollo Client and urql.

What you'll learn

  • How to enable the pg_graphql extension in Supabase
  • How to query and mutate data through the GraphQL endpoint
  • How to integrate Apollo Client with Supabase's GraphQL API
  • How RLS policies apply to GraphQL queries
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minSupabase (all plans), pg_graphql extension, @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

Supabase provides a built-in GraphQL API powered by the pg_graphql extension. Enable it in the SQL Editor, then query your database tables through the GraphQL endpoint at /graphql/v1 using standard GraphQL syntax. The API auto-generates types, queries, and mutations from your database schema, respects RLS policies, and works with any GraphQL client including Apollo Client and urql.

Using Supabase's Built-in GraphQL API with pg_graphql

Supabase includes pg_graphql, a PostgreSQL extension that auto-generates a GraphQL schema from your database tables and exposes it at your project's /graphql/v1 endpoint. This means you do not need a separate GraphQL server. This tutorial walks you through enabling the extension, querying data with GraphQL, setting up mutations for inserts and updates, and integrating with Apollo Client for a full frontend workflow. All GraphQL operations respect your existing RLS policies.

Prerequisites

  • A Supabase project with at least one table created
  • RLS policies configured on your tables
  • Access to the SQL Editor in the Supabase Dashboard
  • Basic familiarity with GraphQL query syntax

Step-by-step guide

1

Enable the pg_graphql extension

The pg_graphql extension is available on all Supabase projects but needs to be enabled. Open the SQL Editor in the Dashboard and run the create extension command. Once enabled, Supabase automatically generates a GraphQL schema from all tables in the public schema that have RLS enabled. The GraphQL endpoint becomes available at https://<project-ref>.supabase.co/graphql/v1.

typescript
1-- Enable the pg_graphql extension
2create extension if not exists pg_graphql;
3
4-- Verify it is installed
5select * from pg_extension where extname = 'pg_graphql';

Expected result: The pg_graphql extension appears in the pg_extension table. The GraphQL endpoint at /graphql/v1 is now active.

2

Query data using the GraphQL endpoint

Send GraphQL queries to the /graphql/v1 endpoint using any HTTP client. Include your Supabase API key in the apikey header and the user's JWT in the Authorization header for authenticated requests. The auto-generated schema creates a collection type for each table (e.g., todosCollection for a todos table) with edges and nodes following the Relay connection specification.

typescript
1// Query using fetch
2const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!
3const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
4
5const response = await fetch(`${SUPABASE_URL}/graphql/v1`, {
6 method: 'POST',
7 headers: {
8 'Content-Type': 'application/json',
9 'apikey': SUPABASE_ANON_KEY,
10 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
11 },
12 body: JSON.stringify({
13 query: `
14 query GetTodos {
15 todosCollection {
16 edges {
17 node {
18 id
19 task
20 is_complete
21 created_at
22 }
23 }
24 }
25 }
26 `
27 })
28})
29
30const { data } = await response.json()
31console.log(data.todosCollection.edges)

Expected result: The response returns a data object with todosCollection.edges containing an array of node objects, each representing a row from the todos table.

3

Insert and update data with GraphQL mutations

pg_graphql auto-generates mutation types for INSERT, UPDATE, and DELETE operations. Insert mutations use insertIntoCollection, update mutations use updateCollection, and delete mutations use deleteFromCollection. All mutations respect RLS policies, so the authenticated user must have the appropriate INSERT, UPDATE, or DELETE policy on the table.

typescript
1// Insert a new todo
2const insertQuery = `
3 mutation InsertTodo {
4 insertIntotodosCollection(objects: [{
5 task: "Learn GraphQL with Supabase"
6 is_complete: false
7 }]) {
8 records {
9 id
10 task
11 is_complete
12 }
13 }
14 }
15`
16
17// Update a todo
18const updateQuery = `
19 mutation UpdateTodo {
20 updatetodosCollection(
21 filter: { id: { eq: 1 } }
22 set: { is_complete: true }
23 ) {
24 records {
25 id
26 task
27 is_complete
28 }
29 }
30 }
31`
32
33// Delete a todo
34const deleteQuery = `
35 mutation DeleteTodo {
36 deleteFromtodosCollection(
37 filter: { id: { eq: 1 } }
38 ) {
39 records {
40 id
41 }
42 }
43 }
44`

Expected result: Each mutation returns the affected records with the fields you requested. The database is updated according to the mutation type.

4

Set up Apollo Client with Supabase GraphQL

For a production frontend, use Apollo Client to manage GraphQL state, caching, and subscriptions. Configure the Apollo HttpLink to point to your Supabase GraphQL endpoint and include the required authentication headers. When the user signs in, update the Authorization header with their JWT.

typescript
1import { ApolloClient, InMemoryCache, HttpLink, ApolloProvider } from '@apollo/client'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7)
8
9const httpLink = new HttpLink({
10 uri: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/graphql/v1`,
11 headers: {
12 apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
13 },
14 fetch: async (uri, options) => {
15 const { data: { session } } = await supabase.auth.getSession()
16 const headers = options?.headers as Record<string, string> || {}
17 if (session?.access_token) {
18 headers['Authorization'] = `Bearer ${session.access_token}`
19 } else {
20 headers['Authorization'] = `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`
21 }
22 return fetch(uri, { ...options, headers })
23 }
24})
25
26const apolloClient = new ApolloClient({
27 link: httpLink,
28 cache: new InMemoryCache()
29})

Expected result: Apollo Client is configured and sends authenticated GraphQL requests to the Supabase endpoint. Data is cached locally for fast re-renders.

5

Filter and paginate GraphQL queries

The auto-generated schema includes filter arguments for each collection that support eq, neq, gt, lt, gte, lte, in, and like operators. Pagination uses the first and after arguments following the Relay cursor specification. Combine filters with ordering using the orderBy argument to build precise queries.

typescript
1const filteredQuery = `
2 query FilteredTodos($cursor: Cursor) {
3 todosCollection(
4 filter: { is_complete: { eq: false } }
5 orderBy: [{ created_at: DescNullsLast }]
6 first: 10
7 after: $cursor
8 ) {
9 edges {
10 node {
11 id
12 task
13 created_at
14 }
15 cursor
16 }
17 pageInfo {
18 hasNextPage
19 endCursor
20 }
21 }
22 }
23`

Expected result: The query returns the first 10 incomplete todos ordered by creation date, with cursor information for fetching the next page.

Complete working example

supabase-graphql-client.ts
1import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7)
8
9const httpLink = new HttpLink({
10 uri: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/graphql/v1`,
11 headers: { apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! },
12 fetch: async (uri, options) => {
13 const { data: { session } } = await supabase.auth.getSession()
14 const headers = (options?.headers as Record<string, string>) || {}
15 headers['Authorization'] = `Bearer ${
16 session?.access_token ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
17 }`
18 return fetch(uri, { ...options, headers })
19 }
20})
21
22export const apolloClient = new ApolloClient({
23 link: httpLink,
24 cache: new InMemoryCache()
25})
26
27// Example: Fetch todos
28export const GET_TODOS = gql`
29 query GetTodos($first: Int, $after: Cursor) {
30 todosCollection(first: $first, after: $after) {
31 edges {
32 node { id task is_complete created_at }
33 cursor
34 }
35 pageInfo { hasNextPage endCursor }
36 }
37 }
38`
39
40// Example: Insert a todo
41export const INSERT_TODO = gql`
42 mutation InsertTodo($task: String!) {
43 insertIntotodosCollection(objects: [{ task: $task, is_complete: false }]) {
44 records { id task is_complete }
45 }
46 }
47`
48
49// Example: Update a todo
50export const UPDATE_TODO = gql`
51 mutation UpdateTodo($id: BigInt!, $isComplete: Boolean!) {
52 updatetodosCollection(
53 filter: { id: { eq: $id } }
54 set: { is_complete: $isComplete }
55 ) {
56 records { id task is_complete }
57 }
58 }
59`

Common mistakes when using Supabase with GraphQL

Why it's a problem: Querying the GraphQL endpoint without the apikey header, getting a 401 Unauthorized error

How to avoid: Always include the apikey header with your SUPABASE_ANON_KEY in every GraphQL request, in addition to the Authorization header.

Why it's a problem: Expecting table names to map directly to GraphQL types without the Collection suffix

How to avoid: pg_graphql appends Collection to table names. A table called todos becomes todosCollection in queries. Use the GraphQL schema explorer to see the exact type names.

Why it's a problem: Not enabling RLS on tables, causing the GraphQL API to return empty results for all queries

How to avoid: Tables without RLS enabled are not exposed through the GraphQL API. Enable RLS and add at least a SELECT policy for the data to appear.

Why it's a problem: Using the service role key in client-side Apollo configuration, bypassing all RLS policies

How to avoid: Always use the SUPABASE_ANON_KEY for client-side requests. The service role key should only be used in server-side code.

Best practices

  • Enable RLS on all tables before querying via GraphQL — tables without RLS are not exposed in the GraphQL schema
  • Use the anon key for unauthenticated queries and the user's JWT for authenticated queries
  • Implement cursor-based pagination with first and after for large datasets instead of loading all records
  • Use Apollo Client's InMemoryCache to reduce redundant network requests for the same data
  • Add a custom fetch function to Apollo's HttpLink to dynamically inject the user's access token
  • Check the auto-generated schema in the Supabase Dashboard's GraphQL playground before writing queries
  • Never expose the service role key in client-side code — it bypasses all RLS policies

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I want to use GraphQL with my Supabase project instead of the REST API. Show me how to enable pg_graphql, query data with filters and pagination, run mutations for CRUD operations, and set up Apollo Client with proper authentication headers.

Supabase Prompt

Enable the pg_graphql extension on a Supabase project and set up Apollo Client to query the GraphQL endpoint at /graphql/v1. Include query examples with collection filters, cursor-based pagination, insert mutations, and update mutations. Show how to pass the user's JWT for authenticated requests.

Frequently asked questions

Do I need to install a separate GraphQL server to use GraphQL with Supabase?

No. Supabase includes the pg_graphql extension which auto-generates a GraphQL API from your database schema. The endpoint is available at /graphql/v1 on every Supabase project.

Does the GraphQL API respect Row Level Security policies?

Yes. All GraphQL queries and mutations go through the same RLS enforcement as the REST API. The user's JWT determines which Postgres role (anon or authenticated) is used.

Can I use subscriptions with Supabase GraphQL?

pg_graphql does not support GraphQL subscriptions natively. For real-time data, use Supabase's Realtime feature with the JS client's channel.on('postgres_changes') API alongside your GraphQL queries.

How do I see the auto-generated GraphQL schema?

In the Supabase Dashboard, navigate to the API section and look for the GraphQL playground. You can also use an introspection query from Apollo Studio or any GraphQL IDE to explore the schema.

Should I use GraphQL or the REST API with Supabase?

The REST API via the JS client is simpler for most use cases and has better TypeScript type generation. GraphQL is beneficial when you need precise field selection, complex nested queries, or you are already using Apollo Client in your project.

Why are some of my tables not showing up in the GraphQL schema?

Tables must be in the public schema and have RLS enabled to appear in the GraphQL schema. Tables in other schemas or without RLS are excluded by default.

Can RapidDev help integrate GraphQL with my Supabase-powered application?

Yes. RapidDev can configure pg_graphql, set up Apollo Client with proper authentication, write optimized queries, and implement caching strategies for your GraphQL-powered Supabase application.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.