supabase_typegen

Generates typed Supabase table definitions from your database schema, so query results never expose raw Map<String, dynamic> data.

For every table the generator emits:

  • a zero-cost row extension type over the decoded JSON map with typed getters,
  • Insert and Update value types that enforce required columns at the construction site and are the only values the typed insert, upsert and update methods accept. Read-only relations such as materialized views get neither, so those methods cannot be called on them,
  • a PostgrestTable definition that carries the schema of the table, so client.table(Books.table) queries the right schema without a .schema() call, and PostgrestColumn tokens for compile-time checked filters and orderings, with nullable columns as PostgrestNullableColumn so isNull() only exists where it can match, and range columns typed PostgrestRange<int>, PostgrestRange<num>, PostgrestRange<PostgrestDate> or PostgrestRange<DateTime> so the range operators only exist on them,
  • PostgrestDate, PostgrestTime and PostgrestInterval for date, time and timetz, and interval columns, value types that keep what a DateTime or Duration cannot: a calendar date without a time of day, a time of day with its UTC offset, and months, days and clock time kept apart,
  • PostgrestToOneRelation and PostgrestToManyRelation members for every foreign key between two generated tables of one schema, named after the table on the other side. A relation goes into a selectOnly list and the embedded rows are read back through it, book.read(Books.authors)?.read(Authors.name). When two keys point at the same table the embed carries the constraint hint and is aliased to the member name, so each comes back under its own key. Keys into another schema and self-referential keys get no member, since PostgREST resolves embeds within the schema of the request only and needs a computed relationship to embed a table into itself,
  • Dart enums for Postgres enums, with wire-name mapping.

The schemas are generated into one file. Objects of the public schema are named after themselves, books becomes Books, BooksRow, BooksInsert and BooksUpdate; objects of any other schema carry the schema as a prefix, so inventory.books becomes InventoryBooks and InventoryBooksRow, and its condition enum InventoryCondition.

Usage

Add supabase_typegen as a dev dependency of your project and run it with dart run supabase_typegen, or install it globally with dart install supabase_typegen and run it as supabase_typegen. The tool connects through the Supabase CLI, so have it installed and, for hosted projects, logged in with supabase login. Then point it at your database:

# The database of the running local Supabase stack (`supabase start`).
dart run supabase_typegen --local

# The project linked with `supabase link`, or any project by ref.
dart run supabase_typegen --linked
dart run supabase_typegen --project-ref abcdefghijklmnopqrst

# Any Postgres database.
dart run supabase_typegen --db-url 'postgresql://postgres:…@db.…supabase.co:5432/postgres'

All of them write lib/supabase_schema.g.dart; pass --output to change the path or --output - to print the code. The types reflect the current state of the database: with --local the SQL in your supabase/ directory stays the single source of truth, since the CLI applies your migrations to the local database and this tool generates from the result, while the other modes generate from whatever that database currently contains. The code is formatted with the dart format of the SDK running the tool, for the language version of the project it is written into (the lower bound of the environment.sdk constraint of the nearest pubspec.yaml), so dart format in that project leaves it unchanged; with --output - the project of the current directory decides.

--linked and --project-ref reach the database through the Management API with your supabase login credentials, so no database password is needed; --project-ref needs a CLI that accepts it on db query (2.116 or newer), older ones want supabase link --project-ref <ref> followed by --linked. --db-url is handed to the CLI as is; it requires TLS unless the connection string says sslmode=disable.

The schemas are chosen the way supabase gen types chooses them: public plus the api.schemas of the project's supabase/config.toml, found under SUPABASE_WORKDIR or by walking up from the working directory, so the exposed schemas of the project are generated. Pass --schema to name the schemas yourself, repeated or comma separated (--schema public,inventory). Only schemas exposed through the Data API can be queried at runtime. Use --import to change which library the generated file imports PostgrestTable and PostgrestColumn from.

How it works

The tool introspects the database with a Dart port of the introspection of @supabase/postgrest-typegen into the GeneratorMetadata intermediate representation its TypeScript, Go, Swift, and Python generators consume, ordered with sortGeneratorMetadata, and generates the Dart code from that document. The queries run through supabase db query, so the CLI resolves and authenticates the connection. The port is pinned to a revision of the TypeScript package and produces a document with the same records; --dump-metadata prints it instead of the generated code, which helps when reporting a generator issue.

The built-in introspection, and with it the --local, --linked, --project-ref, --db-url and --dump-metadata options, is a stopgap. It will be removed once the Supabase CLI ships Dart support for supabase gen types, which then becomes the only way to run this tool; see the next section.

The metadata comes from the database catalog, so nullability, database defaults, and identity columns are exact: a NOT NULL column with a default reads as non-nullable but stays optional on insert, and GENERATED ALWAYS columns appear in the row type but not in the insert and update types.

Through the Supabase CLI

Once supabase gen types ships a Dart language, the CLI will run the same introspection in-process and hand the document to this tool over stdin. The direct connection modes above will be removed in the release that follows, so prefer the CLI as soon as it is available:

supabase gen types --lang dart --local > lib/supabase_schema.g.dart

Reading the document from stdin is what the tool does when no connection option is given, so that path already works with a hand-built document.

Generated code in action

final books = await client.table(Books.table)
    .select()
    .where(Books.mood.eq(Mood.happy) & Books.publishedOn.isNull().not())
    .order(Books.createdAt.desc()); // List<BooksRow>

await client.table(Books.table).insert(
  BooksInsert(title: 'A typed row', tags: ['dart']),
);

// Queried in the inventory schema, since the table definition carries it.
final stock = await client.table(InventoryStock.table).select();

// An explicit schema still wins, for identical tables in several schemas.
final archived = await client
    .schema('archive')
    .table(InventoryStock.table)
    .select();

Known limitations

  • Passing null to an Insert/Update parameter omits the column. To write SQL NULL explicitly, use the generated set…ToNull methods, for example BooksUpdate(inPrint: false).setPriceToNull(); they only exist for nullable columns, so nulling a NOT NULL column is a compile error.
  • Array elements are assumed non-null (text[] maps to List<String>), matching the supabase-js type generator; an array containing SQL NULL elements throws when the column is read. Elements convert like a column of their type, so date[] is List<PostgrestDate> and mood[] a list of the generated enum, except for pgvector elements, which stay in their wire representation (List<String>) because a filter could not tell a vector element from a nested array; postgrestVector decodes them.
  • timestamptz values are written back in UTC and naive timestamp values as local wall time. date, time, timetz and interval columns map to PostgrestDate, PostgrestTime and PostgrestInterval, which parse what Postgres emits, including infinity dates and every IntervalStyle, and write themselves back as the literal Postgres accepts.
  • bytea columns map to Uint8List and are written back as hex literals, the format Postgres emits by default; the escape output format is decoded as well.
  • pgvector vector and halfvec columns map to List<double>, carried as the [0.1,0.2] literal PostgREST sends, and their column tokens compare against vector literals. sparsevec columns stay Object?.
  • Foreign keys into another schema get no relation member, since PostgREST only embeds tables of the schema a request addresses. The foreign key itself is still described, so the column is typed like any other.
  • Typed functions (rpc) are not generated yet.

Libraries

introspection
Introspects a Postgres database into the GeneratorMetadata document of @supabase/postgrest-typegen through supabase db query, so types can be generated before supabase gen types produces the document itself.
supabase_typegen
Generates typed Supabase table definitions, row extension types and column tokens from a database schema.