supabase_flutter
Short Intro: Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products.
Flutter package for Supabase .
Supabase is an open source Firebase alternative. We are a service to:
listen to database changes query your tables, including filtering, pagination, and deeply nested relationships (like GraphQL) create, update, and delete rows manage your users and their permissions interact with your database using a simple UI
Status
Alpha: Under heavy development Public Alpha: Ready for testing. But go easy on us, there will be bugs and missing functionality. Public Beta: Stable. No breaking changes expected in this version but possible bugs. Public: Production-ready
Getting Started
Import the package:
import 'package:supabase_flutter/supabase_flutter.dart';
Intialize Supabase before using it:
import 'package:supabase_flutter/supabase_flutter.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
Supabase.initialize(
url: SUPABASE_URL,
anonKey: SUPABASE_ANNON_KEY,
authCallbackUrlHostname: 'login-callback', // optional
debug: true // optional
);
runApp(MyApp());
}
authCallbackUrlHostname is optional. It will be used to filter Supabase authentication redirect deeplink. You need to provide this param if you use deeplink for other features on the app.
debug is optional. It’s enabled by default if you’re running the app in debug mode (flutter run --debug).
Authentication
Using authentication can be done easily.
Email authentication
import 'package:supabase_flutter/supabase_flutter.dart';
void signIn(String email, String password) async {
final response = await Supabase.instance.client.auth.signIn(email: _email, password: _password);
if (reponse.error != null) {
/// Handle error
} else {
/// Sign in with success
}
}
SupabaseAuthState
It helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter…
For more details, take a look at the example here
When using with a nested authentication flow, remember to call startAuthObserver() and stopAuthObserver() before/after navigation to new screen to prevent multiple observers running at the same time. Take a look at the example here
SupabaseAuthRequiredState
It helps you protect route that requires an authenticated user.
For more details, take a look at the example here
signInWithProvider
This method will automatically launch the auth url and open a browser for user to sign in with 3rd party login.
Supabase.instance.client.auth.signInWithProvider(
Provider.github,
options: supabase.AuthOptions(redirectTo: ''),
);
Custom LocalStorage
As default supabase_flutter uses hive plugin to persist user session. However you can use any other plugins by creating a LocalStorage impl.
For example, we can use flutter_secure_storage plugin to store the user session in a secure storage.
// Define the custom LocalStorage implementation
class SecureLocalStorage extends LocalStorage {
SecureLocalStorage() : super(
initialize: () async {},
hasAccessToken: () {
const storage = FlutterSecureStorage();
return storage.containsKey(key: supabasePersistSessionKey);
}, accessToken: () {
const storage = FlutterSecureStorage();
return storage.read(key: supabasePersistSessionKey);
}, removePersistedSession: () {
const storage = FlutterSecureStorage();
return storage.delete(key: supabasePersistSessionKey);
}, persistSession: (String value) {
const storage = FlutterSecureStorage();
return storage.write(key: supabasePersistSessionKey, value: value);
},
);
}
// use it when initializing
Supabase.initialize(
...
localStorage: SecureLocalStorage(),
);
You can use EmptyLocalStorage to disable session persistance:
Supabase.initialize(
...
localStorage: const EmptyLocalStorage(),
);
Deeplink config
Supabase redirect URLs config
Go to your Supabase project Authentication Settings page. You need to enter your app redirect callback on Additional Redirect URLs field.
The redirect callback url should have this format [YOUR_SCHEME]://[YOUR_AUTH_HOSTNAME]
Supabase 3rd party logins config
Follow the guide https://supabase.io/docs/guides/auth#third-party-logins
For Android
Deep Links can have any custom scheme. The downside is that any app can claim a scheme, so make sure yours are as unique as possible, eg. HST0000001://host.com.
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->
<!-- Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="[YOUR_SCHEME]"
android:host="[YOUR_HOST]" />
</intent-filter>
</activity>
</application>
</manifest>
The android:host attribute is optional for Deep Links.
For more info: https://developer.android.com/training/app-links/deep-linking
For iOS
Custom URL schemes can have… any custom scheme and there is no host specificity, nor entitlements or a hosted file. The downside is that any app can claim any scheme, so make sure yours is as unique as possible, eg. hst0000001 or myIncrediblyAwesomeScheme.
For Custom URL schemes you need to declare the scheme in ios/Runner/Info.plist (or through Xcode’s Target Info editor, under URL Types):
<!-- ... other tags -->
<plist>
<dict>
<!-- ... other tags -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>[YOUR_SCHEME]</string>
</array>
</dict>
</array>
<!-- ... other tags -->
</dict>
</plist>
This allows for your app to be started from YOUR_SCHEME://ANYTHING links.
For more info: https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app
Contributing
Fork the repo on GitHub Clone the project to your own machine Commit changes to your own branch Push your work back up to your fork Submit a Pull request so that we can review your changes and merge
Contribute on GitHub
Flutter integration for Supabase. These packages makes it simple for developers to build secure and scalable products. https://github.com/supabase/supabase-flutter 312 forks. 1,039 stars. 13 open issues. Recent commits: test(supabase_flutter): cover deep link errors, generateRawNonce and the resumable upload storage (#2050)Raises the VM line coverage of `supabase_flutter` from 86.1% (397 of 461lines) to 88.7% (409 of 461).## Tests added- A callback link carrying only `error_code` in the fragment, or only`error_description` in the query, is detected as an auth callback andsurfaces the `AuthException`.- A `PlatformException` from the initial link lookup is logged and`Supabase.initialize` still completes.- An error on the app_links link stream is logged.- `Supabase.instance.oauthLauncher` throws a `StateError` after`dispose`.- `generateRawNonce` returns 16 random bytes and a fresh value on everycall.- `AuthAsyncResumableUploadStorage` reads, writes and removes throughthe auth storage.## Lines that stay uncovered- `supabase_passkey.dart`, left out on purpose.- The web branches of `SharedPreferencesAuthAsyncStorage` and`clearAuthUrlParameters`, which only run on web. `storage_web_test.dart`exercises them on chrome, where coverage is not collected.- The `FormatException` and generic `catch` branches around`getInitialLink`, which app_links cannot reach since it parses linkswith `Uri.tryParse`.Only tests change.<!– This is an auto-generated comment: release notes by coderabbit.ai–><!– coderabbit-ai-generated:v1 –>## Summary by CodeRabbit* **Tests*** Expanded coverage for authentication storage write, read, and removalbehavior, plus generated nonce format and uniqueness.* Added checks for auth errors received through deep links and warninglogs when platform link retrieval or event listening fails.* Added coverage confirming that accessing the OAuth launcher afterSupabase is disposed throws an error.<!– end of auto-generated comment: release notes by coderabbit.ai –> , GitHub test(iceberg): cover every table requirement, table update and catalog operation (#2049)Raises the line coverage of `iceberg` from 77.2% (548 of 710 lines) to100% (710 of 710).## Tests added- Every `TableRequirement` and every `TableUpdate` subclass serializesto its wire form, including `AssertReferenceSnapshotId` without asnapshot id.- Nested `StructType`, `ListType` and `MapType` decode and round tripthrough `TableField`.- `TableSchema` decodes `identifier-field-ids`.- `Snapshot` serializes back to its wire form.- `createTableIfNotExists` loads the existing table when the createanswers with a conflict.- `registerTable` posts to `/register` with the metadata location andsends the `X-Iceberg-Access-Delegation` header built from the`accessDelegation` constructor option.- `listTables` sends `pageToken` and `pageSize` and parses`next-page-token`.- `loadTableResult` parses `config`, `storage-credentials` and `refs`,and a parsed `SnapshotReference` serializes back with its ages andminimum snapshots.Only tests change.<!– This is an auto-generated comment: release notes by coderabbit.ai–><!– coderabbit-ai-generated:v1 –>## Summary by CodeRabbit* **Tests*** Expanded automated coverage for table creation and registration,including conflict recovery and access delegation.* Added checks for paginated table listings and loaded-table settings,credentials, and references.* Added coverage for nested data types, schema identifiers, tableupdates and requirements, and snapshot serialization round trips. Thesetests help verify expected behavior across common table operations anddata formats.<!– end of auto-generated comment: release notes by coderabbit.ai –> , GitHub fix(supabase)!: make stream() survive reconnects without losing or corrupting rows (#2028)Resolves[SDK-2337](https://linear.app/supabase/issue/SDK-2337/fixsupabase-make-stream-survive-reconnects-without-losing-or).`stream()` broke or showed wrong data around realtime reconnects. Thisreworks the lifecycle in `SupabaseStreamBuilder`.## Changes1. **A failed fetch no longer ends the stream.** A transient failure isretried with the realtime client's `reconnectAfter` backoff. While theretry is pending, changes are applied to the previous snapshot if thereis one, so live updates keep flowing and nothing piles up in a buffer. Aclient error that a retry cannot fix (a 4xx other than 401, 408 or 429,such as a missing table) is reported and closes the stream, as before.2. **Socket drops are no longer reported.** A `channelError` or`timedOut` status is now reported only when the channel won't rejoin byitself, for example when the server declines the subscription. Drops andjoin timeouts put the channel into its errored state, so it rejoins andthe rows are fetched again. Those are only logged. The `stream()`dartdoc no longer claims the stream closes when the connection closes.3. **The snapshot is taken once replication is live.** A successful joindoes not yet mean the server streams the changes; the server confirmsthat with a `system` event (`extension: postgres_changes`, `status:ok`). The rows are fetched on that event after every join. If the servernever sends it, a fallback fetch runs after the realtime client's`timeout`. Changes that arrive during the fetch are buffered and appliedon top of its result. Each fetch has an id, so the result of asuperseded or cancelled fetch is dropped. If the channel cannot join atall, a `channelError` or `timedOut` before the first snapshot stilltriggers a fetch, so the rows are shown without realtime.4. **A change with `errors` triggers a fetch.** A payload the servercould not send in full (for example `Error 413: Payload Too Large`) nolonger replaces the stored row with a truncated record. It triggers asnapshot fetch. A burst of such changes results in a single follow-upfetch, and a pending retry keeps its backoff. I could not check againsta running server whether the server actually truncates such records. Thefix holds either way.5. **Listening again fetches once.** `_wasSubscribed` is gone, becauseevery join fetches. All per-listen state is reset when the stream islistened to again.6. When the channel closes on its own (for example through`removeChannel`), the stream no longer calls `unsubscribe()` on it asecond time.`MockRealtimeTransport` in `supabase_test` now sends the `system`confirmation after a join with `postgres_changes`, as the server does.The `MIGRATION.md` section "`stream()` keeps running through reconnects"covers the behavior changes, which is why the commit is marked breaking.## Decisions to review**Which failures are reported.** The ticket left this open. This PR doesthe following, and documents it on `stream()`, in the realtime skill andin `MIGRATION.md`:- Connection drops and rejoins are not reported.- A failed fetch is reported once per run of failures, until a fetchsucceeds again. Retries continue in the background, so a `StreamBuilder`can show the error and then recover.- A client error that a retry cannot fix is reported and closes thestream.- A join the server declines is reported as a`RealtimeSubscribeException`. Its status is always `channelError` now,and its dartdoc says so.An `await for` loop still ends at the first reported error, as with anyDart stream.**Telling a drop from a declined join.** This reads the `@internal“RealtimeChannel.isErrored` from the status listener. The channel entersthat state exactly when it schedules a rejoin. A public "will retry"flag on `RealtimeSubscribeStatusChange` would be cleaner, but it wouldwiden this PR into a `supabase_realtime` API change.**Later first rows.** Rows now appear after the connect, the join andthe replication confirmation, instead of at PostgREST latency. That isthe cost of not missing changes. If the realtime endpoint hangs insteadof failing, the first rows wait for the join timeout.## Tests- New `test/stream_reconnect_test.dart` on `MockSupabaseHttpClient` and`MockRealtimeTransport`. It covers: – the fetch after the join, and merging changes that arrive during it- retries after a failed fetch, both the first fetch and one after areconnect – closing on a client error a retry cannot fix – live changes while a retry is pending – a dropped connection that is not reported- a refetch on `errors`, and a single follow-up fetch for a burst ofthem – a single fetch when listening again – a declined subscription being reported- `mock_test.dart` and `stream_filter_test.dart`: their HttpServerrealtime mocks now send the `system` confirmation. The `mock_test.dart`server also waits for the snapshot request before it sends changes, andignores sends to a socket that a finished test already closed.<!– This is an auto-generated comment: release notes by coderabbit.ai–><!– coderabbit-ai-generated:v1 –>## Summary by CodeRabbit* **New Features*** Realtime streams fetch an initial snapshot after streaming begins andrefresh it after reconnecting. Changes received during a fetch areapplied to the returned rows, and incomplete change records trigger afresh snapshot.* **Bug Fixes*** Temporary fetch failures are retried; unrecoverable client errorsclose the stream. Dropped connections and timed-out joins no longerproduce subscription exceptions. * Server-declined subscriptions continue to report an error.<!– end of auto-generated comment: release notes by coderabbit.ai –> , GitHub fix(supabase_test)!: fast failures, clock-aware sessions, a real signedIn event and stubs that match what the SDK sends (#2035)Linear:https://linear.app/supabase/issue/SDK-2357/fixsupabase-test-fast-failures-clock-aware-sessions-a-real-signedinFixes the twelve `supabase_test` findings from the v3 review. Each onehas a regression test in the package suite.## Changes1. **Fast failures.** `testSupabaseClient` turns off retries fordatabase requests and token refreshes by default, and takes a`retryOptions` parameter to turn them back on. Auth sign-in calls werenever retried. `initializeTestSupabase` in`supabase_flutter/testing.dart` gets the same default and parameter,because its docs promise the same defaults as `testSupabaseClient`.2. **`stubError` docs.** The example now says what happens: every failedattempt counts against `times`, retries included. Retries are off in`testSupabaseClient`, so each read fails until `times` runs out.3. **Unmatched requests fail the test.** An unmatched request stillthrows the `StateError`, and it is now also reported to the current zoneas an uncaught error. That fails the test even when auth or functionswrap the error as a `SupabaseTransportException`. A test that expects anunmatched request to fail can pass`MockSupabaseHttpClient(reportUnmatchedRequests: false)`.4. **Clock-aware sessions.** `signInTestUser` and the session stubs readexpiry from `clock.now()`. The stubs build the session when the requestarrives, so `withClock` and fake async clocks apply.5. **Near-future `expiresAt`.** `signInTestUser` no longer goes through`recoverSession`, so the 30 second expiry margin no longer applies. Onlyan `expiresAt` that falls in the past once truncated to the wholeseconds of the `exp` claim is rejected, with an `ArgumentError`.6. **`signedIn` event.** A new `@internal` method,`AuthClient.signInWithSession`, stores the session and emits `signedIn`.`signInTestUser` uses it.7. **`verifyOTP` stub.** `stubSignIn` now answers `POST /verify` as wellas `/token`.8. **Repeated query keys.** `query:` matching uses `queryParametersAll`,so `{'id': 'lt.10'}` matches `.gt('id', 1).lt('id', 10)`.`RecordedRequest.queryParametersAll` is added for assertions.9. **Fixture `aud` and `role`.** `testUserJson` now sends `aud:authenticated` and takes a `role` parameter, which `signInTestUser`passes through.10. **JWT padding.** `unsignedTestJwt` strips base64 padding.11. **Realtime helpers.** Binary broadcast frames are decoded into`sent`, and get a reply when they carry a ref. New `emitPresenceState`and `emitPresenceDiff` helpers. A leave reuses the ref its presencejoined with, so the client removes it.12. **Version range.** `supabase: ^3.0.0-dev.12` replaces the exact pin.No test covers this one, since it is a pubspec constraint. The new`signInWithSession` call needs the next supabase_auth, so the floor hasto move up with the release. The release tooling raises it when itpublishes.The browser broadcast tests in supabase_auth and supabase_flutter nowexpect `signedIn`.Also fixed: the existing `stubError` and `stubStall` tests expected araw `ClientException` or `TimeoutException`, but PostgREST now wrapsboth in a `PostgrestTransportException`.## Breaking for supabase_test users- Tests that relied on `testSupabaseClient` retrying, for example`stubStatuses([503, 503, 200])` expecting success, now need`retryOptions`.- Tests that caught the unmatched-request `StateError` on purpose nowfail.- `signInTestUser` emits `signedIn` instead of `tokenRefreshed`.supabase_test is new in v3 and has no v2 counterpart, so there is noMIGRATION.md section.## Decision for the reviewer`signInTestUser` emits `signedIn` through a new `@internal` hook on`AuthClient`. There is no public way to install a session withoutnetwork traffic and emit `signedIn`. The alternative is a public API,and I don't think one is warranted for a test helper.## Follow-up worth consideringThe zone report works around a deeper issue. Auth, functions andpostgrest catch everything `send()` throws, `Error`s included, andreport it as a transport failure. So a programming error in a customHTTP client is also handled as being offline. Rethrowing `Error`s in theSDK would fix that at the root. It is left out of this PR because itchanges SDK behavior beyond supabase_test. , GitHub fix(supabase_flutter)!: keep cold-start auth links and make initialize safe against concurrent calls and dispose (#2034)Fixes[SDK-2352](https://linear.app/supabase/issue/SDK-2352/fixsupabase-flutter-cold-start-deep-links-can-be-lost-and-initialize).## Changes1. **Cold-start auth links are no longer lost.** `app_links` sends thelink that launched the app only to the first listener of its linkstream. `SupabaseAuth` now subscribes before waiting for the sessionrestore and holds back incoming links until the restore is done. It alsoreads `getInitialLink` explicitly, so the link is exchanged even whenthe app subscribed first. A launch link delivered both ways is exchangedonce. A link the `detectSessionInUriPredicate` throws for is logged andskipped, so it cannot hold back later links.2. **A concurrent `Supabase.initialize` waits for the first one.** Everycall gets the pending future, so a second call no longer returns ahalf-ready instance. A call with another `url` or `publishableKey` failswith a `StateError`. A failed initialization that never created a clientis forgotten, so the next call tries again.3. **`dispose` during `initialize` leaks nothing.**`SupabaseAuth.initialize` stops after the session restore when it wasdisposed, and the link subscription it opened early is cancelled by`dispose`.4. **`WidgetsFlutterBinding.ensureInitialized()` is calledunconditionally**, so a custom `asyncStorage` no longer breaks the`AppLifecycleListener` setup.5. **The launch link is handled the same way on every platform.** It isexchanged right after `initialize` completes, on web as well as onmobile. This is documented on `Supabase.initialize` and in`MIGRATION.md`.6. **`signInWithSSO` takes `authScreenLaunchMode`**, the name`signInWithOAuth` and `linkIdentity` use.7. **`initializeTestSupabase` throws when an earlier instance was notdisposed**, instead of returning it with the earlier test's mocks. Itchecks a new `@internal` getter, `Supabase.isInitializingOrInitialized`.8. **New tests** cover the auto refresh stop and start across lifecyclestates, the Google-on-Android external browser rule, `signInWithSSO`,`linkIdentity`, the web callback URL including the`history.replaceState` cleanup, and restoring a v2 `Session.toJson`payload through `Supabase.initialize`. The cold-start and disposeregression tests fail on the previous code.## Open decisions- **When the launch link is exchanged (item 5).** I first exchanged itinside `initialize` on both platforms, which is what web did. A listenerthat subscribes to `onAuthStateChange` after `await Supabase.initialize`would then never see `signedIn` or `passwordRecovery`, which breakscold-start password recovery on mobile. So both platforms now exchangeit right after `initialize` completes. The cost is on web: code thatread `currentSession` right after `initialize` to find the session ofthe callback now has to wait for the event.- **`dispose` while `initialize` is pending.** The pending future stillcompletes with the now disposed instance rather than failing. Failing itwould turn every test teardown that disposes early into an error foreach caller awaiting it. Callers see `isInitialized` as false. When`initialize` is called again before the disposed one resumes, thedisposed call waits for and returns the new initialization instead.- **What counts as a different configuration (item 2).** Only `url` and`publishableKey` are compared. The option objects have no valueequality, so comparing them would reject repeated calls that passequivalent but non-const options. Other arguments of a second call arestill ignored.## Breaking changes`MIGRATION.md` and the migrate-v3 skill cover the stricter `initialize`,the `initializeTestSupabase` check, the web timing change and the`signInWithSSO` rename. No public symbol was added or removed, so`sdk-compliance.yaml` is unchanged.The `lifecycle_test.dart` case "a pause shorter than the delay keeps thesocket" is timing sensitive and fails locally under load on `main` aswell. It is not touched here.<!– This is an auto-generated comment: release notes by coderabbit.ai–><!– coderabbit-ai-generated:v1 –>## Summary by CodeRabbit* **Behavior Changes*** Repeated `Supabase.initialize` calls with the same configuration nowshare initialization; calls with different settings fail. Dispose theinstance before switching projects.* Auth callback links are handled after saved-session restoration on allplatforms. On web, wait for the `signedIn` or `passwordRecovery` eventbefore reading the session.* `initializeTestSupabase` now fails if a prior instance has not beendisposed.* **Migration*** The v3 migration guidance covers initialization, callback timing, andsession storage updates. * The SSO launch option is now named `authScreenLaunchMode`.<!– end of auto-generated comment: release notes by coderabbit.ai –> , GitHub
Provides the list of the opensource Flutter apps collection with GitHub repository.