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 📚
- Generate Dart Classes
- Example Usage
- Working with Enums
- Working with Multiple Schemas
- Working with JSONB Custom Types
- Column Selection Queries
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[]✅ |
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
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 legacyservice_rolekey. 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, andSUPABASE_ANON_KEYis 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
publicby 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 insupadart.yaml. Until it is, supadart maps it toString/List<String>and prints a warning with the query to list its values. - Enum type
namesare converted toUPPERCASE(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.yamland 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
schemasunder[api]insupabase/config.tomllocally. 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 getterinventory_items. - Generate from a single non-public schema with
schemas: [inventory]to get plain names (Items) for it. mappingstakeschema.tablekeys. Plain keys only apply to the first schema. If two tables would still get the same name, supadart stops and lists them.enumskeys without a schema refer to the first schema:mood: [...]orinventory.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.yamlusingschema.table.columnformat - Custom Dart models must have
fromJson(Map<String, dynamic>)factory andtoJson()method - Auto-generates
fromJson()andtoJson()calls for typed models - Use
isArray: truefor 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()callsUserProfile.fromJson()andTag.fromJson()for arraystoJson()callsprofileData.toJson()andtags.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
Libraries
- config_init
- generators/class/class
- generators/class/converters
- generators/class/copy_with
- generators/class/from_json
- generators/class/generate_map
- generators/class/insert
- generators/class/new
- generators/class/to_json
- generators/class/update
- generators/index
- generators/standalone/client_extension
- generators/standalone/duration_fromstring
- generators/standalone/enums
- generators/standalone/exports
- generators/standalone/geometry_fromjson
- generators/standalone/supadart_abstract_class
- generators/storage/fetch_storage
- generators/storage/storage
- generators/swagger/column
- generators/swagger/schemas
- generators/swagger/swagger
- generators/swagger/table
- generators/swagger/utils
- generators/utils/fetch_swagger
- generators/utils/string_formatters
- generators/utils/supabase_request
- key_check