Supabase’s GraphQL API, powered by pg_graphql, doesn’t just reflect your database schema; it actively generates your API based on it, and it’s far more dynamic than you might initially think.
Let’s see it in action. Imagine a simple users table:
create table public.users (
id uuid primary key default gen_random_uuid(),
username text not null unique,
email text not null unique,
created_at timestamp with time zone default now()
);
With pg_graphql enabled in Supabase, you get a GraphQL API endpoint ready to go, no extra code needed. You can immediately query for users:
query GetUsers {
users {
id
username
email
}
}
And insert a new user:
mutation CreateUser {
insert_users_one(object: {username: "alice", email: "alice@example.com"}) {
id
username
email
}
}
This isn’t just a thin layer over SQL. pg_graphql introspects your PostgreSQL schema, including tables, columns, relationships (foreign keys), and even RLS policies, to build a comprehensive GraphQL schema. It automatically generates queries, mutations, and subscriptions for CRUD operations, and it respects your database constraints like NOT NULL and UNIQUE.
The magic really happens when you introduce relationships. Let’s add a posts table with a foreign key to users:
create table public.posts (
id uuid primary key default gen_random_uuid(),
user_id uuid references public.users(id),
title text not null,
content text,
created_at timestamp with time zone default now()
);
Now, your GraphQL schema is aware of this. You can fetch a user and their posts directly:
query GetUserWithPosts {
users(where: {username: {_eq: "alice"}}) {
id
username
posts {
id
title
created_at
}
}
}
And conversely, fetch a post and its author:
query GetPostWithAuthor {
posts(where: {title: {_eq: "My First Post"}}) {
id
title
author {
id
username
}
}
}
This nested querying is a core benefit, allowing clients to fetch related data in a single round trip, significantly reducing chattiness.
pg_graphql also understands your PostgreSQL data types and maps them to appropriate GraphQL scalars. uuid becomes UUID, text becomes String, timestamp with time zone becomes timestamptz (which often maps to DateTime on the client side).
Row Level Security (RLS) policies are also translated. If you have an RLS policy on the posts table that only allows a user to see their own posts, pg_graphql will enforce this automatically within your GraphQL queries. For example, if you have a policy like ALTER POLICY "Users can view their own posts" ON posts FOR SELECT USING (auth.uid() = user_id);, a GraphQL query for posts will implicitly filter based on the authenticated user’s ID.
The generated schema is highly configurable. You can expose or hide tables and columns by specifying them in the graphql.schemas configuration in your Supabase project settings. For instance, to only expose the users table and specific columns:
{
"graphql": {
"schemas": {
"public": {
"tables": {
"users": {
"columns": {
"id": {},
"username": {},
"email": {}
}
}
}
}
}
}
}
This level of granular control allows you to tailor your API surface precisely to what your frontend applications need, enhancing security and simplifying client-side development.
One of the most powerful aspects, and often overlooked, is how pg_graphql handles many-to-many relationships. If you have join tables and foreign keys set up correctly, pg_graphql will infer and generate the necessary connections and edge types in your GraphQL schema without you having to define them manually. This means complex relational data can be queried intuitively.
The actual implementation of pg_graphql involves a PostgreSQL extension that hooks into the query planning and execution process. When a GraphQL query arrives, it’s parsed and translated into a corresponding SQL query that respects the schema, relationships, and RLS policies. The results are then formatted back into GraphQL.
The next hurdle you’ll likely encounter is understanding how to leverage the generated _by_pk and _by_unique mutations for efficient single-record operations.