Supabase’s automatic TypeScript generation for your API types is less about magic and more about disciplined schema definition.

Let’s see it in action. Imagine you have a users table in Supabase:

CREATE TABLE public.users (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    email text UNIQUE NOT NULL,
    full_name text,
    created_at timestamp with time zone DEFAULT now()
);

And a posts table:

CREATE TABLE public.posts (
    id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    user_id uuid REFERENCES public.users(id) ON DELETE CASCADE,
    title text NOT NULL,
    content text,
    created_at timestamp with time zone DEFAULT now()
);

When you navigate to the "API" section in your Supabase dashboard and click "Generate types," Supabase inspects these tables and their relationships. It then produces a TypeScript file that mirrors your database schema, enabling type-safe interactions with your Supabase backend.

Here’s a snippet of what that generated file might look like for the tables above:

// This file is automatically generated by PostGraphile.
// DO NOT MODIFY THIS FILE DIRECTLY.
// Instead, modify the database schema.

export interface PgTypes {
  // Tables
  users: {
    id: string; // uuid
    email: string; // text
    full_name: string | null; // text
    created_at: string; // timestamp with time zone
  };
  posts: {
    id: number; // bigint
    user_id: string; // uuid
    title: string; // text
    content: string | null; // text
    created_at: string; // timestamp with time zone
  };
}

This generation process is driven by PostGraphile, an open-source tool that transforms your PostgreSQL schema into a GraphQL API. Supabase leverages PostGraphile’s capabilities to provide this convenient type generation. The core idea is that your database schema is the source of truth. By defining your tables, columns, and relationships in PostgreSQL, you implicitly define the shape of your data and, consequently, the structure of your API.

When you use the Supabase client library in your frontend application, it can import these generated types. This means you get compile-time checks for your data fetching and mutations. For example, if you try to access a full_name property on a user object that doesn’t exist in your schema, your TypeScript compiler will flag it as an error before you even run your code.

Let’s say you want to fetch a user and their posts. With the generated types, your code would look something like this:

import { createClient } from '@supabase/supabase-js';
import { PgTypes } from './types'; // Assuming your generated types are here

const supabaseUrl = 'YOUR_SUPABASE_URL';
const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY';

const supabase = createClient<PgTypes>(supabaseUrl, supabaseAnonKey);

async function getUserWithPosts(userId: string) {
  const { data: user, error: userError } = await supabase
    .from('users')
    .select('id, full_name, posts(*)') // The (*) here is a GraphQL-like syntax for selecting all related posts
    .eq('id', userId)
    .single();

  if (userError) {
    console.error('Error fetching user:', userError);
    return;
  }

  // TypeScript knows `user` has `id`, `full_name`, and `posts` (which is an array of Post objects)
  console.log(`User: ${user.full_name}`);
  user.posts.forEach(post => {
    console.log(`- Post title: ${post.title}`);
  });
}

Notice how user.full_name and post.title are perfectly typed. If you mistyped full_name as fullname, TypeScript would immediately complain.

The underlying mechanism for this type generation involves PostGraphile inspecting your PostgreSQL schema’s system catalogs. It queries tables like pg_catalog.pg_class and pg_catalog.pg_attribute to understand table names, column names, data types, and foreign key constraints. This information is then translated into a GraphQL schema, which PostGraphile then uses to generate the TypeScript types. The PgTypes interface in the generated file acts as a blueprint, mapping your database tables and columns to their corresponding TypeScript representations. For instance, a uuid column in PostgreSQL becomes a string in TypeScript, and a bigint becomes a number. Nullable columns are represented with the | null union type.

A subtle but powerful aspect of this system is how it handles relationships. When you define a foreign key constraint, like user_id uuid REFERENCES public.users(id) ON DELETE CASCADE in the posts table, PostGraphile infers this relationship. This allows you to perform nested queries, fetching related data in a single API call, and the generated types will reflect these nested structures. The posts(*) syntax in the supabase.from('posts').select(...) call is a direct consequence of this relational inference.

The true power of this system lies in its direct correlation to your database schema. Any change you make to your database tables—adding a column, altering a type, or defining a new relationship—can be reflected in your TypeScript types by regenerating the file. This ensures that your frontend code is always in sync with your backend data model, significantly reducing runtime errors and improving developer productivity.

The next step in mastering Supabase’s API generation is understanding how to customize the GraphQL schema that PostGraphile generates, allowing for more advanced querying patterns and schema transformations.

Want structured learning?

Take the full Supabase course →