Skip to content

moinsen-dev/supabase_client_gen

Repository files navigation

supabase_client_gen

Contract-driven code generator for Supabase. Define your backend in a single YAML file, then generate a complete typed Dart client — models, enums, repositories, edge function clients, and storage bucket clients.

# Install globally:
dart pub global activate supabase_client_gen

# Generate from any project:
dart pub global run supabase_client_gen:generate \
  --contract docs/contracts/supabase.yaml \
  --output lib/generated

Why?

Writing Supabase client code by hand means repeating yourself across tables, keeping Dart types in sync with Postgres columns, and wiring up realtime subscriptions manually. This tool reads a single contract file — your backend's source of truth — and produces all the Dart code you need.

One contract. One command. Zero drift.

The same contract always produces byte-identical output (it does not depend on whether a database is reachable), so generated code is safe to commit and gate in CI.

Installation

dart pub global activate supabase_client_gen

Or add as a dev dependency:

dev_dependencies:
  supabase_client_gen: ^0.2.0

Runtime dependencies in the consuming app

Generated code imports the following — add them to the app that consumes the generated client:

dependencies:
  supabase_flutter: ^2.8.0   # repositories, edge functions, storage
  equatable: ^2.0.0          # models

What Gets Generated

Output Path Description
Data models models/ Equatable classes with fromJson/toJson/copyWith, null-safe
Enums enums/enums.dart Dart enums from Postgres enum types, with fromString
Repositories repositories/ Typed select/insert/update/delete/stream
Edge function clients edge_functions/edge_functions.dart Typed wrappers over functions.invoke
Storage clients storage/storage.dart Per-bucket upload/download/URL with MIME + size checks

Every generated file carries a // GENERATED by supabase_client_gen header and must not be hand-edited.

Nullability

Nullability comes only from the contract's nullable_fields lists — this is what keeps generated output deterministic and machine-independent.

To keep those lists honest against the real database, let the DB write them back into the contract:

dart pub global run supabase_client_gen:generate \
  --contract docs/contracts/supabase.yaml --output lib/generated \
  --with-db --sync-nullability --db-url "$SUPABASE_DB_URL"

--sync-nullability reads live column nullability and updates nullable_fields in the contract (preserving comments/formatting). Generation then reads from the contract as usual. Use validate --with-db to detect drift without writing.

The Contract Format

A supabase.yaml file is the single source of truth for your backend. A complete, runnable example lives at example/supabase.yaml. The top-level shape:

contract:
  name: my_project_supabase_contract
  version: "0.1.0"
  date: "2026-01-01"

# Connection details live under project.remote — name and ref are required.
project:
  remote:
    name: my_project
    ref: abcdefghijklmnop

auth:
  provider: supabase
  planned_sign_in_methods:
    - email

data_model:
  public:
    things:
      ownership: workspace
      primary_key: id            # honoured by update/delete/stream
      fields:
        id: uuid
        workspace_id: uuid       # presence makes the repo workspace-scoped
        name: text
        category: thing_category
        notes: text
      nullable_fields:
        - notes
      enum_values:
        thing_category: [device, document, other]
      client_access:
        select: member_of_workspace
        insert: member_of_workspace
        update: member_of_workspace
        delete: member_of_workspace

storage:
  buckets:
    avatars:
      public: true
      allowed_mime_types: [image/png, image/jpeg]
      file_size_limit_mb: 5

edge_functions:
  runtime: deno                  # non-mapping keys like this are ignored
  resolve_thing:
    method: POST
    required_request_fields: [query]
    optional_request_fields: [workspace_id]

realtime:
  publication:
    allowed_tables:
      - public.things

Malformed contracts fail with a path-qualified message (e.g. data_model.public.things.primary_key is required) rather than a stack trace.

Field Types

Contract Type Generated Dart Type
uuid, text String
integer / int4, bigint / int8 int
numeric / decimal double
boolean / bool bool
timestamptz / timestamp / date DateTime
jsonb / json Map<String, dynamic>
vector(N) List<double>
Custom enum Named Dart enum

CLI Reference

generate

dart pub global run supabase_client_gen:generate \
  --contract <path> \          # Path to supabase.yaml
  --output <dir> \             # Output directory for generated code
  [--check] \                  # Verify generated code is up to date (exit 1 if not)
  [--with-db] \                # Connect to the DB (only meaningful with --sync-nullability)
  [--sync-nullability] \       # Write live-DB nullability back into the contract
  [--db-url <url>] \           # postgres:// connection string (or SUPABASE_DB_URL env)
  [--version]

validate

dart pub global run supabase_client_gen:validate \
  --contract <path> \
  --output <dir> \
  [--ts <path>] \              # Path to supabase.types.ts
  [--migrations <dir>] \       # Supabase migrations directory
  [--mode=<mode>] \            # db | types | ts | migrations | all (default)
  [--with-db] [--db-url <url>] [--json]

Four validation checks:

  1. DB ↔ Contract — tables, columns, types, enums, and nullability aligned?
  2. Contract ↔ Generated Code — is the Dart client up to date?
  3. Contract ↔ TS Types — does supabase.types.ts match the contract?
  4. Migration Freshness — any migrations newer than the contract?

Integration with Melos

# melos.yaml
scripts:
  gen:client:
    run: dart pub global run supabase_client_gen:generate
      --contract ../../docs/contracts/supabase.yaml --output lib/generated

  gen:client:check:
    run: dart pub global run supabase_client_gen:generate
      --contract ../../docs/contracts/supabase.yaml --output lib/generated --check

  validate:all:
    run: dart pub global run supabase_client_gen:validate
      --contract ../../docs/contracts/supabase.yaml --output lib/generated
      --ts ../../docs/generated/supabase.types.ts
      --migrations ../../supabase/migrations --mode=all --with-db

Generated API

Repository

A workspace-scoped things table (it has a workspace_id column) yields:

final repo = ThingRepository(supabase.client);

final things = await repo.select(workspaceId: wsId, limit: 50, offset: 0);
final created = await repo.insert({'workspace_id': wsId, 'name': 'Drill'});
final updated = await repo.update(id, {'name': 'Hammer drill'});
await repo.delete(id);

// Realtime (table listed under realtime.publication.allowed_tables):
repo.stream(workspaceId: wsId).listen((things) => print(things));

update/delete/stream use the table's declared primary_key. Tables without a workspace_id column get unscoped select() / stream(). Read-only tables (all writes edge_function_only) get no mutation methods, but still get a .stream() if realtime is enabled.

Edge function client

Each client-invoked edge function becomes a typed top-level function:

final result = await resolveThing(query: 'cordless drill', workspaceId: wsId);
// result is Map<String, dynamic> from the function response

Storage bucket

final avatars = AvatarsBucket(supabase.client);

await avatars.upload('user/$id.png', bytes, contentType: 'image/png');
final url = avatars.getPublicUrl('user/$id.png'); // signed URL for private buckets

Uploads are validated against the bucket's allowed_mime_types and file_size_limit_mb before hitting the network.

Programmatic API

import 'dart:io';
import 'package:supabase_client_gen/supabase_client_gen.dart';

void main() {
  final contract = loadContract('docs/contracts/supabase.yaml');
  final files = ClientGenerator(contract).generate();
  for (final entry in files.entries) {
    File('lib/generated/${entry.key}')
      ..createSync(recursive: true)
      ..writeAsStringSync(entry.value);
  }
}

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors