supadart 2.1.0 copy "supadart: ^2.1.0" to clipboard
supadart: ^2.1.0 copied to clipboard

CLI that generates typesafe Dart and Flutter classes from your Supabase database schema, with converters, insert/update helpers, enums and storage buckets.

Pub Version Pub Points GitHub Stars Runtime Test GitHub License

Supadart 🎯 #

Typesafe Supabase Flutter Queries
Generate Flutter / Dart 🎯 classes from your Supabase schema.

// allBooks is a typeof List<Books>
final allBooks = await supabase
      .books
      .select("*")
      .withConverter(Books.converter);

Table of Contents 📚 #

Features 🚀 #

  • 🛠️ Typesafe Queries (Create, Read, Equality)
  • 🧱 Immutable Generated Classes
  • 🗂️ Roundtrip Serialization fromJson to toJson and back
  • 📊 Supports Column Selection Queries
  • 🔢 Supports all Supabase Major datatypes
  • 🗂️ Supports Defined as array types
  • 🗂️ Supports Enums
  • 🎯 Supports Custom Dart Models for JSONB Columns

Conversion Table 📊 #

Supabase Identifier PostgreSQL Format JSON Type Dart Type Runtime Tested
# int2 smallint integer int type ✅ type[]✅
# int4 integer integer int type ✅ type[]✅
# int8 bigint integer BigInt type ✅ type[]✅
# float4 real number double type ✅ type[]✅
# float8 double precision number double type ✅ type[]✅
# numeric numeric number num type ✅ type[]✅
{} json json object Map<String, dynamic> type ✅ type[]✅
{} jsonb jsonb object Map<String, dynamic> type ✅ type[]✅
T text text string String type ✅ type[]✅
T varchar character varying string String type ✅ type[]✅
T uuid uuid string String type ✅ type[]✅
🗓️ date date string DateTime type ✅ type[]✅
🗓️ time time without time zone string DateTime type ✅ type[]✅
🗓️ timetz time with time zone string DateTime type ✅ type[]✅
🗓️ timestamp timestamp without time zone string DateTime type ✅ type[]✅
🗓️ timestamptz timestamp with time zone string DateTime type ✅ type[]✅
🕒 interval interval string Duration type ✅ type[]✅
💡 bool boolean boolean bool type ✅ type[]✅
🗂️ ENUMS ENUM string Enum type ✅ type[]✅

Other Types

PostGIS Types

Generating Dart Classes #

1. Pre-requisites #

1.2 Do you have serial types?

We don't recommend using serial types when using this package, if you have serial types you need to add a [supadart:serial] to the column like this

You probably don't have them Serial types aren't available in the Supabase editor and must be added via SQL editor manually.

COMMENT ON COLUMN test_table.bigserialx IS '[supadart:serial]';
COMMENT ON COLUMN test_table.smallserialx IS 'you can still add comment [supadart:serial]';
COMMENT ON COLUMN test_table.serialx IS 'this part [supadart:serial] just needs to be included';
-- otherwise the insert method will always ask for a value even though serial types are auto-generated

Why do we need this?

1.3 Install Internationalization package

# This is an official package from dart and is used for parsing dates
flutter pub add intl
# or
dart pub add intl

Unless you are not using any date types, you can skip this step

1.4 Use snake casing for table names and column names (Optional)

this tool will automatically convert snake_case to camelCase for both table (GeneratedClassName) and column (FieldName) generated names.

snake_case  => camelCase
user_table  => UserTable
snake_case  => camelCase
user_id     => userId

2. Generate Dart Classes #

Installation

# 🎯 Active from pub.dev
dart pub global activate supadart

# 🚀 Run via
supadart
# or
dart pub global run supadart

Quick Start


# Initialize supadart.yaml config file
supadart --init

# Generate classes
supadart --url <supabase_url> --key <supabase_secret_key>

# if SUPABASE_URL and SUPABASE_API_KEY are set in the environment variables
# if SUPABASE_URL and SUPABASE_API_KEY are set in supadart.yaml
supadart

# Generate from schemas other than public (the first keeps plain names)
supadart --schema public,inventory

API KEY: Use a secret key (sb_secret_...) or the legacy service_role key. Since April 8, 2026, hosted Supabase projects no longer expose the schema to anon/publishable keys (changelog). Never ship this key in your app or commit it. Keep it in a gitignored .env. Local Supabase stacks still accept the anon/publishable key, and SUPABASE_ANON_KEY is still read as a fallback.

ENUMS: Enums are read from your database. Only enums used solely in array columns need to be listed in the config file (details)

JSONB CUSTOM TYPES: If you want to map JSONB columns to custom Dart model types, you need to specify them in the config file

SCHEMAS: Classes are generated from public by default. Other schemas must be exposed through the Data API (details)

CLI Usage

-h, --help       Show usage information
-i, --init       Initialize config file supadart.yaml
-c, --config     Specify a path to config file of yaml   (default: ./supadart.yaml)
-u, --url        Supabase URL                            (if not set in yaml)
-k, --key        Supabase secret key (sb_secret_...)     (if not set in yaml)
-s, --schema     Schemas to generate, comma separated     (if not set in yaml)
-v, --version

Using the Web App (deprecated)


Example Usage #

Assuming the following table schema

create table
  public.books (
    id bigint generated by default as identity,
    name character varying not null,
    description text null,
    price integer not null,
    created_at timestamp with time zone not null default now(),
    constraint books_pkey primary key (id)
  ) tablespace pg_default;

1. Use the CLI or the Web App to generate dart classes #

class Books implements SupadartClass<Books> {
  final BigInt id;
  final String name;
  final String? description;
  final int price;
  final DateTime? createdAt;

  const Books({
    required this.id,
    required this.name,
    this.description,
    required this.price,
    this.createdAt,
  });

  static String get table_name => 'books';
  static String get c_id => 'id';
  static String get c_name => 'name';
  static String get c_description => 'description';
  static String get c_price => 'price';
  static String get c_createdAt => 'created_at';

  static List<Books> converter(List<Map<String, dynamic>> data) {
    return data.map(Books.fromJson).toList();
  }

  static Books converterSingle(Map<String, dynamic> data) {
    return Books.fromJson(data);
  }

  static Map<String, dynamic> _generateMap({
    BigInt? id,
    String? name,
    String? description,
    int? price,
    DateTime? createdAt,
  }) {
    return {
      if (id != null) 'id': id.toString(),
      if (name != null) 'name': name.toString(),
      if (description != null) 'description': description.toString(),
      if (price != null) 'price': price.toString(),
      if (createdAt != null) 'created_at': createdAt.toUtc().toString(),
    };
  }

  static Map<String, dynamic> insert({
    BigInt? id,
    required String name,
    String? description,
    required int price,
    DateTime? createdAt,
  }) {
    return _generateMap(
      id: id,
      name: name,
      description: description,
      price: price,
      createdAt: createdAt,
    );
  }

  static Map<String, dynamic> update({
    BigInt? id,
    String? name,
    String? description,
    int? price,
    DateTime? createdAt,
  }) {
    return _generateMap(
      id: id,
      name: name,
      description: description,
      price: price,
      createdAt: createdAt,
    );
  }

  factory Books.fromJson(Map<String, dynamic> json) {
    return Books(
      id: json['id'] != null
          ? BigInt.parse(json['id'].toString())
          : BigInt.from(0),
      name: json['name'] != null ? json['name'].toString() : '',
      description:
          json['description'] != null ? json['description'].toString() : '',
      price: json['price'] != null ? json['price'] as int : 0,
      createdAt: json['created_at'] != null
          ? DateTime.tryParse(json['created_at'].toString()) as DateTime
          : DateTime.fromMillisecondsSinceEpoch(0),
    );
  }

  Map<String, dynamic> toJson() {
    // Promotion doesn't work well with public fields due to the possibility of the field being modified elsewhere.
    return _generateMap(
      id: id,
      name: name,
      description: description,
      price: price,
      createdAt: createdAt,
    );
  }
}

2. Using the generated class #

we now have a typesafe'ish to interact with the database.

Fetch Data

// allBooks is a typeof List<Books>
final allBooks = await supabase
      .books
      .select("*")
      .withConverter(Books.converter);

Fetch Single Data

// book is a typeof Books
final book = await supabase
      .books
      .select("*")
      .eq(Books.c_id, 1)
      .single()
      .withConverter(Books.converterSingle);

Insert Data

// Yes we know which one's are optional or required.
final data = Books.insert(
  name: 'Learn Flutter',
  description: 'Endless brackets and braces',
  price: 2,
);
await supabase.books.insert(data);

Inset Many Data

final many_data = [
  Books.insert(
    name: 'Learn Minecraft',
    description: 'Endless blocks and bricks',
    price: 2,
  ),
  Books.insert(
    name: 'Description is optional',
    created_at: DateTime.now(),
    price: 2,
  ),
];
await supabase.books.insert(many_data);

Update Data

final newData = Books.update(
  name: 'New Book Name',
);
await supabase.books.update(newData).eq(Books.c_id, 1);

Delete Data

await supabase.books.delete().eq(Books.c_id, 1);

Working with Enums #

Enums are read from your database, so most need no configuration.

IMPORTANT:

  • PostgREST's schema only lists enum values for non-array columns. An enum used only in array columns (e.g. mood[]) must be listed in supadart.yaml. Until it is, supadart maps it to String / List<String> and prints a warning with the query to list its values.
  • Enum type names are converted to UPPERCASE (mood → MOOD)
  • Each enum keeps its database label in .value. Labels that are not valid Dart identifiers get a converted name: 'in-progress' → inProgress, '2fa' → v2fa, 'default' → default_
  • If supadart.yaml and the database disagree on an enum's values, the database wins and supadart warns

Assuming the following schema

CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral', 'excited', 'angry');
CREATE TABLE enum_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    mood mood NOT NULL,
    past_moods mood[] NULL
);

mood is found automatically through the mood column. If it were only used in past_moods, you would list it in your supadart.yaml:

enums:
  # Case sensitive, define them as they are in the database
  # Get them with: SELECT unnest(enum_range(NULL::public.mood));
  mood: [happy, sad, neutral, excited, angry]

Generated Enum #

enum MOOD {
  happy('happy'),
  sad('sad'),
  neutral('neutral'),
  excited('excited'),
  angry('angry');

  const MOOD(this.value);

  /// The label as stored in the database.
  final String value;

  static MOOD fromValue(String value) =>
      values.firstWhere((e) => e.value == value);
}

Create / Read / Update with Enums #

MOOD firstEnumVal = MOOD.angry;
MOOD newEnumVal = MOOD.excited;

// Create
await supabase.enum_types.insert(EnumTypes.insert(
      mood: firstEnumVal,
    ));

await supabase.enum_types
        // Update
        .update(EnumTypes.update(mood: newEnumVal))
        // Filters take the database label
        .eq(EnumTypes.c_mood, firstEnumVal.value);

// Read
await supabase.enum_types.select().withConverter(EnumTypes.converter);

Working with Multiple Schemas #

supadart generates from public unless told otherwise. List the schemas in supadart.yaml, or pass --schema (which overrides the yaml):

schemas:
  - public
  - inventory

IMPORTANT:

  • Each schema must be exposed through the Data API: Project Settings > Data API > Exposed schemas on hosted projects, or schemas under [api] in supabase/config.toml locally. supadart tells you when one is not.
  • The first schema keeps plain names. Tables and enums of the others are prefixed with their schema, so names never clash: inventory.items → InventoryItems, inventory.mood → INVENTORY_MOOD, client getter inventory_items.
  • Generate from a single non-public schema with schemas: [inventory] to get plain names (Items) for it.
  • mappings take schema.table keys. Plain keys only apply to the first schema. If two tables would still get the same name, supadart stops and lists them.
  • enums keys without a schema refer to the first schema: mood: [...] or inventory.status: [...].

The generated client getters select the schema for you, and every class has a schema_name:

// SELECT * FROM inventory.items
final items = await supabase.inventory_items
    .select()
    .withConverter(InventoryItems.converter);

// Equivalent, without the extension
await supabase
    .schema(InventoryItems.schema_name)
    .from(InventoryItems.table_name)
    .select();

Working with JSONB Custom Types #

Maps JSONB columns to custom Dart model types instead of Map<String, dynamic> or dynamic.

IMPORTANT:

  • Specify JSONB column mappings in supadart.yaml using schema.table.column format
  • Custom Dart models must have fromJson(Map<String, dynamic>) factory and toJson() method
  • Auto-generates fromJson() and toJson() calls for typed models
  • Use isArray: true for JSONB fields containing JSON arrays [{...}, {...}]

Configuration #

jsonb:
  # Single object (default)
  public.users.profile_data:
    type: UserProfile
    import: "package:my_app/models/user_profile.dart"

  # Array of objects (requires isArray: true)
  public.users.tags:
    type: Tag
    import: "package:my_app/models/tag.dart"
    isArray: true

Generated Output #

class Users implements SupadartClass<Users> {
  final UserProfile profileData;  // single object
  final List<Tag>? tags;           // array (isArray: true)
  // ...
}

The generated code automatically handles conversion:

  • fromJson() calls UserProfile.fromJson() and Tag.fromJson() for arrays
  • toJson() calls profileData.toJson() and tags.map((e) => e.toJson()).toList()

Usage #

// Insert
await supabase.users.insert(Users.insert(
  profileData: UserProfile(name: 'John', age: 30),
  tags: [Tag(id: 1, name: 'developer')],
));

// Read
final users = await supabase.users.select("*").withConverter(Users.converter);
print(users.first.profileData.name);  // "John"

Column Selection Queries #

IMPORTANT:

  • When you select columns, the class will fill the missing columns with the default values
final book = await supabase
      .from('books')
      .select('${Books.c_id}, ${Books.c_name}')
      .eq(Books.c_id, 69) // Assuming 69 is the id
      .single()
      .withConverter(Books.converterSingle);
print(book.id);           // 69
print(book.name);         // "Supadart"
print(book.description);  // ""
print(book.price);        // 0
print(book.created_at);   // 1970-01-01 00:00:00.000

if a value is an enum, the first value of that enum will be used as the default value

Contributors #

GitHub contributors

10
likes
140
points
1.33k
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

CLI that generates typesafe Dart and Flutter classes from your Supabase database schema, with converters, insert/update helpers, enums and storage buckets.

Repository (GitHub)
View/report issues

Topics

#cli #generator #supabase #schema #class

License

MIT (license)

Dependencies

args, dotenv, http, yaml

More

Packages that depend on supadart