Skip to main content

supabase-flutter-idioms

The current supabase_flutter v3 API for writing or reviewing Dart code that talks to Supabase: client setup, typed queries with table(), typed rpc, auth, realtime streams, error handling and logging. Use whenever code imports supabase_flutter, or the user asks to query, insert, subscribe, sign in or handle errors with Supabase. Prevents v2 patterns such as from() with maps, anonKey, and realtime callbacks.

Datos de origen

Repositorio
supabase/supabase-flutter
Última actividad en el origen
23 de octubre de 2026 a las 12:51
Idioma detectado de SKILL.md
inglés
Estrellas
1040
Forks
313

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
supabase-flutter-idioms
description
The current supabase_flutter v3 API for writing or reviewing Dart code that talks to Supabase: client setup, typed queries with table(), typed rpc, auth, realtime streams, error handling and logging. Use whenever code imports supabase_flutter, or the user asks to query, insert, subscribe, sign in or handle errors with Supabase. Prevents v2 patterns such as from() with maps, anonKey, and realtime callbacks.
# Writing supabase_flutter v3 code Most published Supabase Dart examples are v2 code. v3 changed many of the APIs those examples use, so do not copy a pattern from memory without checking it against this file. To upgrade an existing v2 project, use the `supabase-flutter-migrate-v3` skill instead. ## Rules - Query tables with `table()` and the generated schema, never with `from()` and `Map<String, dynamic>`, unless the table is missing from the generated file. - Generate the schema with the Supabase CLI (2.122.0 or newer) and never edit it by hand. Regenerate it after every database migration: `supabase gen types --lang dart --local > lib/supabase_schema.g.dart`. The `supabase-typegen-generate` skill, installed with the `supabase_typegen` dev dependency, covers the setup and the options. - Pass `publishableKey`. `anonKey` does not exist in v3. - Listen to realtime with streams. v3 has no `callback:` parameters. - Catch the `SupabaseException` hierarchy, never `ClientException`, `TimeoutException` or `String`. - A typed `update()` or `delete()` does nothing until `.where(…)` or `.all()` is called on it. The analyzer reports a missing one as an `unused_result` warning. - Use one client, `Supabase.instance.client`, for the whole app. ## Client setup ```dart file import 'package:flutter/widgets.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: 'https://your-project.supabase.co', publishableKey: 'your-publishable-key', ); runApp(const SizedBox()); } final supabase = Supabase.instance.client; ``` `Supabase.initialize` persists the session with `SharedPreferencesAsync` and restores it before it returns. Pass `FlutterAuthClientOptions(asyncStorage: …)` to store it elsewhere, for example in secure storage. ## Reading rows The generated file has, per table, a row type (`BooksRow`), insert and update types (`BooksInsert`, `BooksUpdate`) and a class (`Books`) with the table definition and one constant per column and relation. A table in another schema carries the schema as a prefix, `InventoryBooks`. ```dart final List<BooksRow> books = await supabase .table(Books.table) .select() .where(Books.inPrint.isTrue() & Books.price.lt(10)) .order(Books.createdAt.desc()) .limit(20); final String title = books.first.title; final BooksRow book = await supabase .table(Books.table) .select() .where(Books.id.eq(1)) .single(); ``` Filters are methods on the column constants, combined with `&`, `|` and `not()`. Values must match the column type, and `isNull()` only exists on nullable columns. `order()` sorts ascending unless the ordering says `.desc()`. Read some columns only, or embed related rows, with `selectOnly`, and read the values back through the same constants: ```dart final books = await supabase.table(Books.table).selectOnly([ Books.title, Books.authors(Authors.name), ]); final String title = books.first.read(Books.title); final String? authorName = books.first.read(Books.authors)?.read(Authors.name); ``` ## Writing rows ```dart await supabase .table(Books.table) .insert(BooksInsert(authorId: 2, title: 'Dune')); final BooksRow inserted = await supabase .table(Books.table) .insert(BooksInsert(authorId: 2, title: 'Emma')) .select() .single(); await supabase .table(Books.table) .update(BooksUpdate(inPrint: false).setPriceToNull()) .where(Books.id.eq(1)); await supabase.table(Books.table).delete().where(Books.id.eq(1)); ``` - Several rows go through `insertAll()` and `upsertAll()`. - A `null` argument to `BooksInsert` or `BooksUpdate` leaves the column out. Write SQL `NULL` with the generated `set…ToNull()` methods. - Writes resolve to `void`. Add `.select()` to get the written rows back. - `upsert()` takes `onConflict` as a list of columns, `onConflict: [Authors.name]`. ## Calling database functions Every function PostgREST exposes gets a typed method on `rpc`: ```dart final BooksRow? book = await supabase.rpc.bookById(bookId: 1); ``` `supabase.rpc('fn', params: {...})` is the untyped fallback for functions the generator gives no method, which it explains in a comment in the generated file. ## Auth Sign-in methods that always produce a session return the `Session`: ```dart final session = await supabase.auth.signInWithPassword( email: 'user@example.com', password: 'a-strong-password', ); final user = session.user; ``` `signUp()` and `verifyOTP()` still return an `AuthResponse`, since they can complete without a session. `AuthState` is sealed, with one subtype per event. Every new listener first receives `AuthInitialSession` with the current session or `null`. A failed token refresh, for example while offline, is an error of the stream, so every listener passes `onError`: ```dart final subscription = supabase.auth.onAuthStateChange.listen( (state) { switch (state) { case AuthInitialSession(:final session): // The session when this listener attached, or null. print(session?.user.id); case AuthSignedIn(:final session): print('Signed in as ${session.user.email}'); case AuthSignedOut(): print('Signed out'); case _: } }, onError: (Object error) => print('Auth error: $error'), ); ``` `user.userMetadata` is never null, and `user.appMetadata` is an `AppMetadata` with typed `provider` and `providers`. ## Realtime Keep a list of rows up to date with `stream()`: ```dart final Stream<List<BooksRow>> books = supabase .table(Books.table) .stream(primaryKey: [Books.id]) .filter(Books.authorId.eq(2)) .order(Books.title); ``` The `supabase-flutter-realtime` skill covers broadcast, presence and channel status in more depth. React to individual changes with `onTableChanges`. Create the stream before `subscribe()`, and remove the channel when done: ```dart final channel = supabase.channel('books'); final subscription = channel .onTableChanges(Books.table, filters: [Books.authorId.eq(2)]) .listen((change) { switch (change) { case TypedPostgresInsert(:final newRow): print('Added ${newRow.title}'); case TypedPostgresUpdate(:final newRow): print('Changed ${newRow.title}'); case TypedPostgresDelete(:final oldRow): print('Removed ${oldRow.read(Books.id)}'); } }); channel.subscribe(); await subscription.cancel(); await supabase.removeChannel(channel); ``` Broadcast and connection status are streams too: ```dart final channel = supabase.channel('room'); channel .onBroadcast(event: 'cursor') .listen((message) => print(message['payload'])); channel.onStatusChange.listen((change) async { if (change.status == RealtimeSubscribeStatus.subscribed) { await channel.sendBroadcastMessage(event: 'cursor', payload: {'x': 1}); } }); channel.subscribe(); ``` The table must be added to the `supabase_realtime` publication in the database, or no changes arrive. Create a stream once in `State`, never inside `build()`, or every rebuild resubscribes: ```dart file import 'package:flutter/widgets.dart'; import 'package:my_app/supabase_schema.g.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class BookList extends StatefulWidget { const BookList({super.key}); @override State<BookList> createState() => _BookListState(); } class _BookListState extends State<BookList> { final _books = Supabase.instance.client .table(Books.table) .stream(primaryKey: [Books.id]); @override Widget build(BuildContext context) { return StreamBuilder<List<BooksRow>>( stream: _books, builder: (context, snapshot) { final books = snapshot.data ?? const []; return Column(children: [for (final book in books) Text(book.title)]); }, ); } } ``` ## Errors Every exception the SDK throws for a failed request extends `SupabaseException`. `SupabaseApiException` means the server answered with an error, and `SupabaseTransportException` means no response arrived, including a timeout: ```dart try { await supabase.table(Books.table).select(); } on SupabaseApiException catch (error) { print('${error.statusCode} ${error.errorCode}: ${error.message}'); } on SupabaseTransportException catch (error) { print('No response: ${error.cause}'); } ``` Each service also has its own base, such as `PostgrestException`, `AuthException` or `StorageException`, to catch the errors of one service only. `AuthApiException` and `PostgrestApiException` carry the server's `errorCode`. ## Logging The SDK never prints. It logs through `package:logging` under the `supabase` logger, so listen to it to see the records: ```dart file import 'package:logging/logging.dart'; void enableSupabaseLogs() { Logger.root.onRecord.listen((record) { if (record.loggerName.startsWith('supabase.')) { print('${record.loggerName}: ${record.level.name}: ${record.message}'); } }); } ``` ## Tests Use the `supabase_test` package as a dev dependency instead of mocking the HTTP client by hand. Its `MockSupabaseHttpClient`, `testSupabaseClient` and `signInTestUser` answer requests and put a client in a signed-in state. The `supabase-test-testing` skill, installed with the `supabase_test` dev dependency, covers stubbing, assertions, auth and realtime in tests.
Ver en GitHub