https://github.com/moinsen-dev/supabase_client_gen
https://github.com/moinsen-dev/supabase_client_gen
Last synced: 18 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/moinsen-dev/supabase_client_gen
- Owner: moinsen-dev
- License: mit
- Created: 2026-06-04T11:23:56.000Z (24 days ago)
- Default Branch: develop
- Last Pushed: 2026-06-04T11:50:17.000Z (24 days ago)
- Last Synced: 2026-06-04T13:22:31.417Z (24 days ago)
- Language: Dart
- Size: 22.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# 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.
```bash
# 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
```bash
dart pub global activate supabase_client_gen
```
Or add as a dev dependency:
```yaml
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:
```yaml
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:
```bash
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`](example/supabase.yaml). The
top-level shape:
```yaml
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` |
| `vector(N)` | `List` |
| Custom enum | Named Dart enum |
## CLI Reference
### generate
```bash
dart pub global run supabase_client_gen:generate \
--contract \ # Path to supabase.yaml
--output \ # 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 ] \ # postgres:// connection string (or SUPABASE_DB_URL env)
[--version]
```
### validate
```bash
dart pub global run supabase_client_gen:validate \
--contract \
--output \
[--ts ] \ # Path to supabase.types.ts
[--migrations ] \ # Supabase migrations directory
[--mode=] \ # db | types | ts | migrations | all (default)
[--with-db] [--db-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
```yaml
# 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:
```dart
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:
```dart
final result = await resolveThing(query: 'cordless drill', workspaceId: wsId);
// result is Map from the function response
```
### Storage bucket
```dart
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
```dart
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](LICENSE).