Skip to content

feat(supabase_flutter)!: use a system web auth session for OAuth, SSO and identity linking#1403

Draft
spydon wants to merge 77 commits into
v3from
feat/oauth-web-auth-session
Draft

feat(supabase_flutter)!: use a system web auth session for OAuth, SSO and identity linking#1403
spydon wants to merge 77 commits into
v3from
feat/oauth-web-auth-session

Conversation

@spydon

@spydon spydon commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Move signInWithOAuth, signInWithSSO and linkIdentity off url_launcher and onto a native system web authentication session via flutter_web_auth_2:

  • iOS and macOS: ASWebAuthenticationSession
  • Android: Custom Tabs
  • Web: full page redirect of the current tab (unchanged behavior, now done via the web package)

The session captures the redirect to the redirectTo scheme, closes itself, and returns the callback URL, which is exchanged with getSessionFromUrl.

Addresses #1402. Fixes #1174.

Why

url_launcher's in-app browser does not dismiss itself when the OAuth redirect returns to the app, leaving the user on a blank page after a successful sign in (#1174). A system web auth session is OS owned, auto-dismisses, and hands the callback back to the caller, so it fixes the dismissal on every platform and lets the call resolve on completion.

Changes

  • Add flutter_web_auth_2; remove url_launcher as a direct dependency and the LaunchMode export.
  • Replace authScreenLaunchMode / launchMode with a preferEphemeral option.
  • Derive the callback scheme from redirectTo; forward host and path for https universal links.
  • Web redirect via a small conditional-import shim over package:web.
  • README updated with the required native config; new oauth_test.dart covering the native flow, preferEphemeral, https universal links, and the missing-redirectTo error.

Breaking changes

  • authScreenLaunchMode / launchMode removed; LaunchMode no longer exported.
  • Android apps must register the flutter_web_auth_2 CallbackActivity for their redirect scheme.
  • The OAuth callback no longer arrives through the app_links deep link handler (magic links, email confirmation and password recovery still do).

Status

Draft. flutter analyze and flutter test pass, but the native and desktop flows have not been verified on device yet. See the open questions in #1402 (forcing the external browser, the web popup vs redirect choice, dropping the Google on Android workaround, desktop callback model).

… and identity linking

Route signInWithOAuth, signInWithSSO and linkIdentity through
flutter_web_auth_2 on native and desktop platforms, so the auth screen
runs in ASWebAuthenticationSession on Apple platforms and Custom Tabs on
Android. The session captures the redirect itself and closes
automatically, fixing the blank in-app browser that was left open after a
successful sign in (#1174). On web the current tab is redirected as before.

url_launcher is dropped as a direct dependency and the LaunchMode export
is removed. The authScreenLaunchMode/launchMode parameters are replaced by
a preferEphemeral option that maps to the web auth session.

BREAKING CHANGE: signInWithOAuth, signInWithSSO and linkIdentity no longer
accept authScreenLaunchMode/launchMode, and the LaunchMode export is
removed. Android apps must register the flutter_web_auth_2 CallbackActivity
for their redirect scheme. The OAuth callback no longer arrives through the
app_links deep link handler.
Pieter-JanRobrechtCronos and others added 24 commits July 7, 2026 10:30
…1536)

> Stacked on #1535. Review/merge that one first.

## What kind of change does this PR introduce?

Bug fix

## What is the current behavior?

`getAuthorizationDetails` crashed with a `FormatException` when the
OAuth 2.1 server short-circuited the consent flow. This happens when a
user who already approved a client starts a new authorization for it:
the server returns a redirect-only body (`{"redirect_url":
"...?code=..."}`) with no user or client, which the parser rejected
because it required a user object.

## What is the new behavior?

`getAuthorizationDetails` now checks whether a `redirect_url` is present
returning `OAuthAuthorizationRedirectResponse` when it is present. When
the key isn't present the old `OAuthAuthorizationDetailsResponse` object
is returned.

Callers now switch on the result and forward the redirect instead of
losing it to an exception. Genuinely malformed detail bodies (no
redirect_url and no user) still throw, so validation isn't weakened.

## Additional context

BREAKING CHANGE: `getAuthorizationDetails` now returns the sealed
`OAuthAuthorizationResponse` instead of
`OAuthAuthorizationDetailsResponse`. Callers must switch/cast to access
client, user, scope and redirectUri.
## What

The `update-version` melos script (run as a `preCommit` hook during
`melos version`) iterated over every `packages/*/` and redirected output
into `$d/lib/src/version.dart`. This failed for `supabase_lints`, which
is a lints-only package with no `lib/src/` directory:

```
ERROR: /bin/sh: line 4: packages/supabase_lints//lib/src/version.dart: No such file or directory
```

The package was added after the script was written and was never
accounted for.

## Fix

Only update packages that already have a `version.dart` file:

```sh
[ -f "${d}lib/src/version.dart" ] || continue
```

This naturally skips `supabase_lints` (no `lib/src/version.dart`) and
`yet_another_json_isolate` (intentionally keeps no tracked
`version.dart`), which also lets us drop the fragile `rm
packages/yet_another_json_isolate/lib/src/version.dart` line.

## Verification

Dry-run of the updated loop writes `version.dart` for the seven packages
that have one and skips both `supabase_lints` and
`yet_another_json_isolate`.
- gotrue@2.26.0
 - realtime_client@2.11.0
 - supabase@2.14.0
 - supabase_flutter@2.16.0
## What

Housekeeping pass over every package to make the published pub.dev
metadata and supporting files consistent and correct.

### pubspec.yaml (all packages)
- Add `topics` (pub.dev tags) to every package.
- Add `issue_tracker` pointing to the monorepo issues.
- Fix `yet_another_json_isolate` metadata that pointed at the stale
`supabase-community/json-isolate-dart` repo
(`homepage`/`repository`/`issue_tracker`).
- Fix the broken `documentation` link in `storage_client`
(`storage-createbucket` → `file-buckets-createbucket`).

### READMEs
- Standardize every package README to the same header format as
`supabase_flutter` (logo, title, description, `Guides · Reference Docs`,
badges).
- Fix stale badge URLs that pointed at archived per-package repos
(`gotrue-dart`, `postgrest-dart`, `realtime-dart`, `storage-dart`,
`supabase-dart`) to the monorepo.
- Use the Supabase wordmark logo (`logo-preview.jpg`). A single `<img>`
is used instead of a `<picture>` element because pub.dev does not honor
`<picture>` / `prefers-color-scheme`, which would leave a white-text
logo invisible on pub.dev's light background. See
dart-lang/pub-dev#6363. The `logo-preview.jpg` asset has a dark card
baked in, so it stays visible on both light and dark backgrounds.
- Add missing `License` sections to `yet_another_json_isolate` and
`supabase_lints`.

### Examples
- Replace the empty `functions_client` example with a real invoke
example.
- Fix the misspelled `annonToken` variable and align naming in the
`gotrue` example.
- Update stale `supabase.io` comment URLs to `supabase.com`.
- Fix a 404 link in the `supabase` example README.

### Not touched
- CHANGELOG files (auto-generated by release tooling).
- LICENSE files (verified, all MIT).

## Notes
- Related upstream issue: dart-lang/pub-dev#6363 (pub.dev support for
`<picture>` / `prefers-color-scheme`).

Resolves SDK-1246
## What

Fixes the WASM incompatibility that lowers `storage_client`'s pub.dev
platform support score (partial 10/20 with the note "Package not
compatible with runtime wasm").

## Why

Two conditional imports were gated on `dart.library.js`:

```dart
import 'file_io.dart' if (dart.library.js) './file_stub.dart';
```

`dart.library.js` refers to the **legacy** JS library, which is **not**
defined under WASM (WASM exposes `dart.library.js_interop`). So a WASM
build evaluates the condition to `false` and falls through to the
default `file_io.dart`, which does `export 'dart:io' show File;`.
`dart:io` is unavailable on web, so WASM compilation fails.

## How

Invert the conditional to the WASM-safe pattern already used by the
other packages (`platform_stub` / `dart.library.io`): default to the
stub, and only import `dart:io` when it actually exists.

```dart
import 'file_stub.dart' if (dart.library.io) './file_io.dart';
```

- WASM / web: `dart.library.io` is false → uses `file_stub.dart` (no
`dart:io`)
- Native (VM, mobile, desktop): `dart.library.io` is true → uses
`file_io.dart`

`storage_client` is a transitive dependency of `supabase` and
`supabase_flutter`, so this clears the WASM incompatibility for those
too.

## Verification

- `dart analyze` passes clean
- `pana` now reports: **WASM-ready: This package is compatible with
runtime `wasm`.**
## What

Adds a `Release Pana` workflow that runs
[pana](https://pub.dev/packages/pana) on every publishable package, but
only on release PRs (title starts with `chore(release):`, created by the
melos versioning action).

## Why

`storage_client` recently shipped a WASM incompatibility that dropped
its pub.dev platform-support score to a partial 10/20 (see #1543).
Nothing in CI caught it before publishing. Release PRs are the last gate
before packages go to pub.dev, so that's where a pana check belongs.

## How

- **Trigger:** `pull_request` (`opened`, `synchronize`, `reopened`,
`edited`), gated by a job-level `if:
startsWith(github.event.pull_request.title, 'chore(release):')`. It is
skipped (cheaply) on all non-release PRs.
- **Setup:** Flutter + Melos (so both Dart and Flutter packages
resolve), then `dart pub global activate pana`.
- **Matrix:** one job per publishable runtime package
(`functions_client`, `gotrue`, `postgrest`, `realtime_client`,
`storage_client`, `supabase`, `supabase_flutter`,
`yet_another_json_isolate`). `supabase_lints` is excluded since
platform/WASM support isn't meaningful for a lint package.
- **Report:** the full pana score table (per section) and tags are
written to the job summary for every package.
- **Hard fail:** if a package reports `platform:web` but not
`is:wasm-ready`. This is the exact regression class from #1543, and
keying off these tags avoids false positives on native-only or
reduced-platform packages.

The jq/tag logic was verified against real `pana --json` output locally.
## What

Bumps the `melos` dev_dependency from `^8.1.0` to `^8.2.0` and updates
`pubspec.lock` accordingly.

## Why

The "Detect affected packages" CI job runs `dart pub global activate
melos` (now 8.2.0) on a Dart-only runner. When the committed lock pinned
8.1.0, the version mismatch made cli_launcher relaunch the pinned melos
via `dart run`, whose implicit `dart pub get` fails on this Flutter
pub-workspace:

```
Because passkeys_example requires the Flutter SDK, version solving failed.
Flutter users should use `flutter pub` instead of `dart pub`.
```

Keeping the pin in sync with the released version means global == local,
so no relaunch happens and the Dart-only job resolves cleanly.

The underlying launcher issue is fixed separately in cli_launcher
(relaunch via `flutter pub run` for Flutter workspaces).
…on/FunctionsHttpException subtypes (#1545)

## What

Introduces three error subtypes as subclasses of the existing
`FunctionException`, so consumers can match on the failure mode:

- **`FunctionsFetchException`** — network/transport failure reaching the
function, before any response is received. The originating error is
available in `details`; `status` is `0`.
- **`FunctionsRelayException`** — error returned by the relay, detected
via the `x-relay-error: true` response header.
- **`FunctionsHttpException`** — non-2xx response from the function
itself.

`invoke()` now throws the appropriate subtype depending on the failure
mode.

## Naming

The subtypes use the `Exception` suffix (not `Error`) to follow Dart's
`Error`/`Exception` distinction: they are meant to be caught, extend
`FunctionException` (which implements `Exception`), and wrap an
underlying `ClientException`. This also keeps them consistent with the
base class name.

## Why non-breaking

Existing `catch` blocks that catch `FunctionException` continue to work
unchanged; the new subtypes only refine the type consumers can
optionally match on. No existing signature or default behavior changes.
`toString()` now uses `\$runtimeType` so subtypes print their own name.

A follow-up for making `FunctionException` a `sealed` class (breaking,
enabling exhaustive `switch`) is tracked in #1550 for v3.

## Tests

Added `network-error` and `relay-error` fixtures to the custom HTTP
client, plus cases asserting each subtype is thrown, that
`status`/`details`/`reasonPhrase` are populated, and that the subtypes
remain catchable as `FunctionException`.

Closes SDK-1258
…dpoint (#1551)

## Summary

An empty or default `TransformOptions()` was routing `download()` and
`getPublicUrl()` through the `/render/image/` endpoint unnecessarily.
Previously the decision was based on `transform != null`, so any
`TransformOptions` instance, even one with all-null fields, switched to
the render path. Now routing only switches to the render endpoint when
the transform options actually produce query parameters.

**Outcome:** implemented (bug fix)

## Behavior

- `download(path)` without transform uses `/object/` endpoint
- `download(path, transform: TransformOptions())` with empty transform
uses `/object/` endpoint
- `download(path, transform: TransformOptions(width: 200))` uses
`/render/image/authenticated/` endpoint
- Same logic for `getPublicUrl()`

## Tests

Added unit tests in `packages/storage_client/test/basic_test.dart`
covering empty vs actual transform routing for both `download()` and
`getPublicUrl()`. All storage_client unit tests pass.

## Reference

- Linear: SDK-878
- supabase-js PR: supabase/supabase-js#2219
(commit `993bb5fc`)

## Compliance matrix

`storage.file_buckets.download_with_transform` and
`storage.file_buckets.get_public_url` remain `implemented`; this fix
brings their routing behavior in line with supabase-js, so no status
change was required.
## Summary

Adds an optional `currentPassword` field to `UserAttributes`. When the
auth server has
`GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_CURRENT_PASSWORD` enabled,
changing a password requires the current password to be sent for
verification.

The field is serialized as `current_password`, which is the key the auth
server reads (`json:"current_password"`). Note that supabase-js
currently sends the raw camelCase `currentPassword`, which the server
does not read; a separate fix has been opened against supabase-js.

## Changes

- `packages/gotrue/lib/src/types/user_attributes.dart` — add nullable
`currentPassword` field, include it in `toJson()` as `current_password`,
and in `==`/`hashCode`.
- `packages/gotrue/test/src/types/user_attributes_test.dart` — tests for
serialization, null-omission, and equality.

## Outcome

Implemented.

## References

- Linear: SDK-753
- supabase-js: PR #2131 / commit f681484
- auth server: supabase/auth PR #2364

## Compliance matrix

`auth.session.update_user` already tracked as `partially_implemented`;
no status change.
#1555)

## Summary

`OAuthProvider` already accepts arbitrary identifiers, so a custom OIDC
provider passed as `OAuthProvider('custom:my-oidc-provider')` already
worked at runtime and was forwarded to the GoTrue API unchanged,
matching supabase-js's `custom:` prefix behavior. This PR does not
change that runtime behavior. It cleans up the internal serialization
and adds coverage and documentation for it.

## What changed

- Switched provider serialization in `signInWithIdToken` and
`getOAuthSignInUrl` from the deprecated `OAuthProvider.snakeCase` getter
to `OAuthProvider.name`. Both return the identifier verbatim, so this is
behavior-neutral.
- Added unit tests confirming custom OIDC provider sign-in URL
generation, with and without options.
- Updated the compliance matrix note under
`auth.sign_in.sign_in_with_oauth` to record that custom OIDC providers
are supported.

## Notes

- No normalization is applied to the provider name. The value is
forwarded to the server as-is, so built-in providers keep working
unchanged and there are no breaking changes.
- The `snakeCase` to `name` change does not affect built-in providers,
because `snakeCase` already returned `name` verbatim.

## Reference

- supabase-js PR: supabase/supabase-js#2134
- Linear: SDK-752
…token (#1565)

## What

Removes the `jwt_decode` dependency from `gotrue`. Its two remaining
uses were both unauthenticated payload decodes, which are now handled by
the existing `decodeJwt` helper (built on `dart_jsonwebtoken`, already a
dependency).

## Changes

- `session.dart` — `Jwt.parseJwt(accessToken)['exp']` →
`decodeJwt(accessToken).payload.exp`
- `gotrue_mfa_api.dart` — `Jwt.parseJwt(session.accessToken)` →
`decodeJwt(session.accessToken).payload.claims` (keeps `aal`/`amr`
lookups working)
- `gotrue_client.dart` — dropped the now-unused `jwt_decode` import
- `client_test.dart` — updated assertions to use
`decodeJwt(...).payload.exp`
- `pubspec.yaml` — removed `jwt_decode: ^0.3.1`

Signature verification via `dart_jsonwebtoken`'s `JWT.verify` is
untouched. Behavior is preserved: both replaced uses were plain payload
decodes without verification.

`dart pub get` resolves cleanly (no `jwt_decode` in the lock) and `dart
analyze lib test` reports no issues.

Closes SDK-475.
…lushing (#1553)

## Summary

When `subscribe()` runs before an asynchronous access token has resolved
(common when the token is read from async storage, e.g. React Native /
Expo, or a third-party `accessToken` callback), the buffered `phx_join`
payload captured at subscribe time carries no `access_token`. When the
connection opens the token-less join is flushed to the server, which
accepts the join but cannot evaluate RLS, so `postgres_changes` events
are silently filtered out.

This ports the supabase-js fix to the Dart realtime client: on
connection open, the access token is now resolved before the send buffer
is flushed. Once auth has settled, buffered channel join payloads are
patched with the resolved token, the stale buffered joins are dropped,
and the join is re-sent for any channel still joining.

## Outcome

**implemented** (bug fix). No public API changes; the fix is internal to
the connect flow in `RealtimeClient`.

## Changes

- `packages/realtime_client/lib/src/realtime_client.dart` —
`_onConnOpen` now delegates to a new async `_resolveAccessTokenAndFlush`
that resolves the token via `setAuth`, patches each channel's join
payload, clears the stale send buffer, and re-sends joins for channels
still joining before flushing.
- `packages/realtime_client/test/socket_test.dart` — new test verifying
a buffered join is re-sent with the resolved token patched into its
payload.
- `sdk-compliance.yaml` — added
`realtime.client.join_payload_access_token` as `implemented`.

## Testing

- `packages/realtime_client` unit tests (`socket_test.dart`,
`channel_test.dart`) pass.
- Realtime integration tests (protocol 1.0.0 and 2.0.0) pass against the
local backend.

## References

- Linear: SDK-754
- supabase-js: PR #2136, commit `c35fba4`
- Compliance matrix: `realtime.client.join_payload_access_token` →
`implemented`
## What

Adds an optional `channel` parameter (`sms` / `whatsapp`) to
`GoTrueMFAApi.challenge()` for phone factors, matching supabase-js.

```dart
await supabase.auth.mfa.challenge(
  factorId: factorId,
  channel: OtpChannel.whatsapp,
);
```

## Why

Parity gap tracked in `sdk-compliance.yaml` (`auth.mfa.challenge` was
`partially_implemented`): there was no way to pick the messaging channel
for phone MFA challenges, so it always used SMS.

## How

- Reuses the existing `OtpChannel` enum (already used by
`signInWithOtp`, `signUp`, `resend`) instead of introducing a new type.
- The parameter is nullable and only added to the request body when
provided, so when omitted the server's existing SMS default applies.
This keeps the change non-breaking; existing callers send the exact same
request as before.
- Updated `sdk-compliance.yaml` to mark `auth.mfa.challenge` as
`implemented`.

## Tests

Added `packages/gotrue/test/mfa_challenge_mock_test.dart` covering the
default (no channel), `whatsapp`, and `sms` cases via a mock HTTP client
that asserts the forwarded request body.

Resolves SDK-1260.
## What

Marks `functions.invocation.timeout` as `not_applicable` in
`sdk-compliance.yaml` instead of implementing it.

## Why

This started as an implementation of a per-invocation `timeout` on
`FunctionsClient.invoke()`. As raised in review, wrapping the returned
`Future` in `Future.timeout` does **not** abort the in-flight request.
So a caller could still see the function succeed after the timeout
fires, and the behavior is identical to the caller applying `.timeout()`
on the result themselves.

Exposing it as an `invoke()` option therefore adds no real capability
and would be misleading, so the implementation is dropped and the
capability is recorded as `not_applicable` with an explanatory note. A
genuinely useful version would require request cancellation (tracked
separately as `functions.invocation.request_cancellation`).

## Changes

- Added a `not_applicable` status to the legend in
`sdk-compliance.yaml`.
- Set `functions.invocation.timeout` to `not_applicable` with a note
explaining the no-op wrapper reasoning.

Closes SDK-1262.
…auth parity implemented (#1548)

## What

Marks `auth.sign_in.sign_in_with_oauth` as `implemented` in the SDK
compliance matrix and documents the URL-without-launch path.

- `sdk-compliance.yaml`: `auth.sign_in.sign_in_with_oauth` moves from
`partially_implemented` to `implemented`.
- `signInWithOAuth` doc comment now points callers to
`getOAuthSignInUrl` when they want the OAuth URL without launching a
browser.

## Why

The compliance gap was "the `skipBrowserRedirect` option is not publicly
exposed." In supabase-js `skipBrowserRedirect` does two things: (1)
suppresses the automatic `window.location.assign(url)` browser
navigation, and (2) adds a `skip_http_redirect` server hint.

In Flutter, half (1) is already the default behavior of
`getOAuthSignInUrl`, which builds and returns the OAuth `OAuthResponse`
(including the URL) without ever launching a browser. That is the
meaningful Flutter equivalent of "skip the browser redirect and hand me
the URL."

Adding an explicit `skipBrowserRedirect` flag was considered and
dropped:
- On `signInWithOAuth` it would only build the URL and discard it, since
that method returns `Future<bool>` (launch success), not the URL.
- On `getOAuthSignInUrl` it would only toggle the `skip_http_redirect`
server hint, which is a niche flag that makes the server return JSON
instead of a 302, undesirable for the common flow of opening the URL in
a browser/webview, and oddly named on a method that never touches a
browser.

So the capability is satisfied by the existing API; this PR reclassifies
it and adds a doc pointer.

## Non-breaking

Documentation and compliance-metadata only; no API or behavior change.

Closes SDK-1261
…extension (#1562)

## What

Four OAuth client enums in `packages/gotrue/lib/src/types/types.dart`
hardcoded a `value` field that was just the snake_case of the entry
name. This drops that duplication and derives the wire value from the
existing `Enum.snakeCase` extension instead.

## Changes

`types.dart`: Convert `OAuthClientGrantType`, `OAuthClientResponseType`,
`OAuthClientType`, and `OAuthClientRegistrationType` to plain enums
(drop the `value` field and constructors) and replace every `.value` use
with `.snakeCase` across `fromString`, `OAuthClient.fromJson`, and the
`toJson` methods.

Enums with values that deviate from a clean snake_case conversion
(`ErrorCode`'s `...Disabled` → `..._not_enabled`, `AMRMethod`'s
slash-separated values, `AuthChangeEvent`'s SCREAMING_SNAKE) were
intentionally left untouched.

## Testing

- `dart analyze lib` clean
- `dart test test/src/gotrue_oauth_api_test.dart` passes (8/8)
## What

Removes the `rxdart` dependency from `gotrue`. It was used in exactly
one place: the two `BehaviorSubject<AuthState>` controllers backing
`onAuthStateChange` and `onAuthStateChangeSync`.

They are replaced with a small, private `_ReplaySubject<T>` built on top
of the standard `dart:async` primitives (`StreamController.broadcast` +
`Stream.multi`).

## Why

`rxdart` was pulling in a whole reactive-extensions library for a single
feature: latest-value replay for late subscribers.

## Preserved behavior

- **Latest-value replay** — a new subscriber immediately receives the
current `AuthState` on `.listen()`.
- **`sync: true`** delivery for `onAuthStateChangeSync` (synchronous
event ordering).
- **Error propagation** — network errors are still emitted as stream
errors (and the last error is replayed to late subscribers).
- **Broadcast semantics** — multiple simultaneous listeners.

The forwarding of live events is done synchronously through the
per-listener `Stream.multi` controller so the underlying broadcast
controller's scheduling is the only hop. This keeps delivery latency
identical to `BehaviorSubject` (a naive double-controller would add an
extra microtask, which broke an ordering-sensitive test).

## Testing

- Full `gotrue` unit suite passes; the remaining failures are
pre-existing integration tests that require a running GoTrue server
(confirmed identical failures on `main`).
- Stream behavior (replay, broadcast, sync delivery, error
replay/propagation, done-on-close) validated against `BehaviorSubject`
semantics.

Closes SDK-1278.
## What

Marks `database.configuration.request_timeout` as `not_applicable` in
`sdk-compliance.yaml`, and adds the `not_applicable` status to the
legend.

## Why

The canonical capability requires cancelling in-flight requests once the
deadline is reached. Over Dart's `http.Client` transport there is no
cancellation primitive, so the only Dart-native option is
`Future.timeout`, which lets the request keep running. That is
functionally identical to a caller applying `.timeout()` on the result
themselves (the PostgREST builder's existing `.timeout()` already
behaves this way), so exposing it as a construction-time option adds no
real capability and would be misleading.

This is the same reasoning applied to `functions.invocation.timeout` in
#1546. A genuinely useful version would require
`database.using_modifiers.request_cancellation`, which stays tracked as
`not_implemented`.

## Changes

- Added the `not_applicable` status to the legend in
`sdk-compliance.yaml`.
- Set `database.configuration.request_timeout` to `not_applicable` with
an explanatory note.
## Summary

SDK-701 tracks a supabase-js bug fix (PR supabase/supabase-js#1763,
commit f99a58c) in the WebAuthn MFA enrollment flow:
`mfa.webauthn.enroll` runs the WebAuthn ceremony inline in the browser
and patches the credential creation options' `user.name` before calling
`navigator.credentials.create()`. The fix corrects the fallback naming
so a provided `friendlyName` is used, falling back to user data only
when it is absent.

## Outcome: not_applicable

The Dart SDK does not perform the client-side WebAuthn ceremony inline.
It:

- exposes only the server side of the ceremony through the `passkey` API
in `gotrue` (get options / verify credential), which never touches
`user.name`;
- delegates the device ceremony to a platform passkey plugin, with
`supabase_flutter`'s `passkey_options_mapper.dart` handing the raw
server options to the plugin;
- rejects `webauthn` in `mfa.enroll` (already documented on
`auth.mfa.enroll`), routing WebAuthn through the passkey API instead;
- has no `friendlyName` parameter on passkey registration, as the server
generates the friendly name.

There is therefore no client-side option-patching step where the SDK-701
bug could exist, and the `friendlyName` fallback cannot be reproduced
faithfully in the passkey idiom. This is a JS/browser-ceremony concern
with no meaningful Dart equivalent.

## Compliance matrix

Added `auth.mfa.webauthn_enroll` → `not_applicable` with a note
explaining the divergence, and documented the `not_applicable` status in
the header comment block.

Ref: SDK-701
## What

Modernizes dependency and `dev_dependency` constraints across the whole
workspace to the newest versions that still resolve on the declared
minimum Dart SDK of `>=3.9.0`, as tracked in SDK-1279. The SDK
constraint itself is unchanged.

## Dependency bumps

- Raised floors on direct dependencies: `http` `^1.6.0`, `logging`
`^1.3.0`, `collection` `^1.19.0`, `crypto` `^3.0.7`, `meta` `^1.16.0`,
`retry` `^3.1.2`, `rxdart` `^0.28.0`, `mime` `>=2.0.0`, `http_parser`
`^4.1.0`, `web` `>=1.0.0`, `web_socket_channel` `>=3.0.0`,
`dart_jsonwebtoken` `>=3.0.0`, `url_launcher` `^6.3.2`,
`shared_preferences` `^2.5.5`.
- Development tooling: `lints` and `flutter_lints` `^4.0.0` to `^6.0.0`,
`mocktail` `^1.0.5`, `postgres` `^3.5.0`, `dotenv` `^4.2.0`, `otp`
`^3.2.0`, `mason_logger` `^0.3.5`.
- `app_links` floor raised to `>=6.4.1` while keeping the `<8.0.0` upper
bound. Version 7.x requires Dart `>=3.10`, so the floor stays resolvable
on the 3.9.0 minimum while consumers on newer SDKs still get 7.x.

## Staying resolvable on the minimum Flutter 3.35 SDK

Local development runs on a newer Dart, so a few packages had to be held
below their latest release to remain resolvable on Flutter 3.35 (Dart
3.9.0), which is the minimum this repository supports:

- `test` is capped at `^1.25.0`. On Flutter 3.35, `flutter_test` pins
`test_api 0.7.6`, and `test >=1.31.0` requires a newer `test_api` (and
Dart `>=3.10`). This floor resolves to the 1.26.x line on the minimum
SDK and to the latest elsewhere.
- `async` `^2.12.0` and `path` `^1.9.0` stay at or below the versions
vendored by Dart 3.9.0.

## Consolidating on supabase_lints

- Every example now depends on `supabase_lints` instead of
`flutter_lints` or `lints` directly, and includes the shared analysis
options.

## Follow-on lint and analyzer fixes

- `lints` 6 introduces `unnecessary_underscores`, so `(_, __, ___)`
wildcard parameters in `realtime_client` were collapsed to `(_, _, _)`.
- `gotrue`: use a null-aware map element (available since Dart 3.8) and
drop inferrable type arguments to satisfy DCM.
- `supabase_lints`: disable `prefer-shorthands-with-constructors`, which
requires the still-experimental dot shorthands, alongside the other
shorthand rules already disabled for the 3.9.0 floor.
- `supabase_flutter` example: switching it to `supabase_lints` surfaced
real DCM findings, now fixed in code (guard `setState` behind `mounted`
after awaits, extract async button handlers so callbacks stay
synchronous, and cancel the auth stream subscription). Only
`prefer-single-widget-per-file` is relaxed, since keeping a single-file
demo in one file is intentional.

## Verification

- `melos bootstrap`, `melos analyze` (`--fatal-infos`), `melos format
--set-exit-if-changed`, and `dcm analyze packages` all pass across the
13 packages.
- `dart pub outdated` no longer reports any dependency held back by our
own constraints. The remaining entries are pinned by the Flutter and
Dart SDK environment.
- Mock and unit test suites pass: `yet_another_json_isolate`,
`postgrest` (172, run serially), `functions_client` (41),
`supabase_flutter` (59), and the `realtime_client` channel tests (54).

The server-backed integration suites (gotrue, storage, realtime socket,
supabase) require the docker test infrastructure with per-run database
reset and are validated in CI.
…flag (#1558)

## Summary

Implements the two `partially_implemented` client initialization options
tracked by SDK-1272:

- **`FlutterAuthClientOptions.detectSessionInUriPredicate`** — an
optional `bool Function(Uri)` predicate that decides whether an incoming
deep link should be treated as an auth callback and exchanged for a
session. When null, the existing default heuristic (checks for
`access_token`, `code`, `error`, `error_code`, `error_description` in
the query or fragment) is used. This lets apps disambiguate redirects
when they use those same parameters for other purposes.
- **`FlutterAuthClientOptions.persistSession`** — a real flag (default
`true`). When `false` and no `localStorage` is supplied, sessions are
kept in memory only (`EmptyLocalStorage` is selected automatically), so
opting out of persistence no longer requires manually wiring up an
`EmptyLocalStorage` backend. Supplying a custom `localStorage` still
takes precedence.

Both are new optional initialization options, so existing
`Supabase.initialize` calls are unaffected.

## Outcome

implemented

## Reference

Derived from the compliance matrix (`sdk-compliance.yaml`); mirrors
supabase-js `detectSessionInUrl` / `persistSession` GoTrue client
options.

## Tests

Added to `packages/supabase_flutter/test/deep_link_test.dart`:
- predicate returning `false` suppresses detection of an otherwise valid
auth callback
- predicate governs detection based on the incoming URI (and receives
the URI)
- `persistSession: true` persists to the default storage
- `persistSession: false` keeps the session in memory only

All 63 `supabase_flutter` tests pass; `melos format` and `dart analyze`
clean.

## Compliance matrix

- `client.authentication_integration.session_url_detection` →
`implemented`
- `client.session_management.persist_session` → `implemented`

SDK-1272
## Summary

Implements the postgrest parity ticket SDK-1269, bundling three additive
query-builder capabilities from the SDK compliance matrix.

- **`dryRun()`** — new `PostgrestTransformBuilder.dryRun()` modifier
that appends `Prefer: tx=rollback`, so the query runs but the
transaction is rolled back and no changes are persisted (supabase-js
`rollback()`).
- **`stripNulls()`** — new `PostgrestTransformBuilder.stripNulls()`
modifier that appends `;nulls=stripped` to the `Accept` header, omitting
null-valued properties from the response (requires PostgREST 11.2+).
- **rpc `head`/`count`** — no code change. In Dart these are already
available through the type-safe `.head()` / `.count()` chaining, which
is the idiomatic builder pattern. Inline call options cannot preserve
the three distinct return types (`PostgrestFilterBuilder<T>`,
`PostgrestBuilder<void>`,
`ResponsePostgrestBuilder<PostgrestResponse<T>>`) since Dart has no
method overloading, so the matrix entry is reconciled to `implemented`
with a note.

## Outcome

implemented

## Reference

Canonical capability registry: `supabase/sdk`
`capabilities/database.yaml` (`dry_run`, `strip_nulls`). supabase-js
`rollback()` in `postgrest-js` `PostgrestTransformBuilder.ts`.

## Compliance matrix

- `database.using_modifiers.dry_run` → `implemented`
- `database.using_modifiers.strip_nulls` → `implemented`
- `database.query.rpc` → `implemented` (was `partially_implemented`)

## Tests

Added header-assertion and backend-behavior tests in
`packages/postgrest/test/transforms_test.dart`. Full postgrest suite
passes (176 tests) against a local `supabase start` backend.

SDK-1269
## What

Removes the `rxdart` dependency from the `supabase` package. It was used
in exactly one place: `SupabaseStreamBuilder`, which backed its stream
controllers with rxdart's `BehaviorSubject`.

They are replaced with a small, private `_BehaviorSubject<T>` built on
top of the standard `dart:async` primitives
(`StreamController.broadcast` + `Stream.multi`).

## Why

`rxdart` was pulling in a whole reactive-extensions library for a single
feature: latest-value replay for late subscribers. This mirrors what
#1566 already did for `gotrue`, and removes `rxdart` from the monorepo
entirely.

Note: #1566 only dropped rxdart from `gotrue`; the `supabase` package
used it independently, and #1567 merely bumped its version constraint.

## Preserved behavior

- **Latest-value replay** — a new subscriber immediately receives the
last emitted event on `.listen()`.
- **Error propagation** — errors are still emitted as stream errors (and
the last error is replayed to late subscribers).
- **Broadcast semantics** — multiple simultaneous listeners.
- `onListen` / `onCancel` lifecycle hooks used to lazily start/tear down
the realtime + postgrest fetch.

## Testing

`dart analyze` clean and all `supabase` package tests pass
(mock_test.dart, client_test.dart).
spydon and others added 29 commits July 14, 2026 15:23
## What

Implements the Storage **Vector Buckets** subsystem in `storage_client`,
closing all 14 `storage.vector_buckets.*` gaps in the compliance matrix.

Reachable through `supabase.storage.vectors`, mirroring the supabase-js
`StorageVectorsClient` structure while staying idiomatic to the Dart
storage client (operations return parsed data directly and throw
`StorageException` on failure instead of a `{data, error}` envelope).

```dart
final vectors = supabase.storage.vectors;
await vectors.createBucket('embeddings');

final bucket = vectors.from('embeddings');
await bucket.createIndex(
  name: 'documents',
  dimension: 1536,
  distanceMetric: DistanceMetric.cosine,
);

final index = bucket.index('documents');
await index.putVectors([
  Vector(key: 'doc-1', data: [0.1, 0.2, 0.3], metadata: {'title': 'Intro'}),
]);

final result = await index.queryVectors(queryVector: [0.1, 0.2, 0.3], topK: 5);
```

## Capabilities

Bucket & index management (`SupabaseVectorsClient`,
`StorageVectorBucketApi`):
- create / list / delete vector bucket, get bucket metadata
- scope to a bucket via `.from(name)` and to an index via `.index(name)`
- create / get / list / delete index

Vector data operations (`StorageVectorIndexApi`):
- put / get / list / query / delete vectors
- parallel scan via `listVectors`' `segmentCount` / `segmentIndex`

## Idiomatic Dart choices

- `dataType` and `distanceMetric` are enums (`VectorDataType`,
`DistanceMetric`), parsed leniently (unknown values decode to `null` for
forward compatibility).
- `metadataConfiguration` is flattened to a `nonFilterableMetadataKeys`
parameter.
- Batch sizes (put/delete 1-500) and parallel-scan segments (1-16) are
validated client-side with `ArgumentError`.

## Tests

- 20 self-contained unit tests using the mock HTTP client, verifying
request URLs/bodies and response parsing for every operation, plus the
validation paths.

## Compliance matrix

- All 14 `storage.vector_buckets.*` features set to `implemented` with
every new public symbol registered. The symbol check and compliance-file
validator both pass locally.

Closes SDK-1307.
## Summary

Adds `signInWithWeb3` support to `gotrue`, wrapping the GoTrue `POST
/token?grant_type=web3` endpoint used for Web3 wallet authentication
(Sign-In with Ethereum and Sign-In with Solana, both derived from
EIP-4361).

- `GoTrueClient.signInWithWeb3({required Web3Chain chain, required
String message, required String signature, String? captchaToken})`
exchanges a signed message for a session, saves it, and emits
`AuthChangeEvent.signedIn`.
- New `Web3Chain` enum (`ethereum`, `solana`).
- Exposed through `supabase` / `supabase_flutter` automatically via the
existing `auth` getter.

Message signing and wallet interaction are delegated to the caller
(using `web3dart`, `walletconnect_dart`, or a Solana wallet library).
This matches the platform-agnostic path in supabase-js:
`window.ethereum` / `window.solana` auto-detection has no Flutter
equivalent, so the SDK does not build the message or auto-detect a
browser wallet.

## Outcome

**implemented** — for `Web3Chain.ethereum` the signature is a hex
string, for `Web3Chain.solana` it is a base64url string, matching the
wire format supabase-js sends.

## Tests

`packages/gotrue/test/web3_auth_test.dart` (mocked, 6 tests, all
passing): Ethereum happy path, Solana happy path, captcha token, bad
signature, expired nonce, and network error.

## Reference

supabase-js / auth-js `GoTrueClient.signInWithWeb3` (`POST
/token?grant_type=web3`).

## Compliance matrix

`auth.sign_in.sign_in_with_web3`: `not_implemented` → `implemented`
(symbols: `GoTrueClient.signInWithWeb3`, `Web3Chain`).

Linear: SDK-836
…mes (#1604)

## Summary

Removes the explicit `<String, String>` type arguments from the
`_platformNames` const map in
`packages/supabase_common/lib/src/client_info.dart`. The type is
inferred identically from the string literals, so this is a no-op at
runtime.

## Why

The DCM check (`CQLabs/setup-dcm` installs the latest DCM at runtime)
now flags this as `avoid-inferrable-type-arguments`. Because the DCM
version is not pinned, the rule surfaces intermittently and can turn the
DCM job red on unrelated PRs. This removes the finding at the source.

No behavior change.
…#1599)

## What

Reconciles `sdk-compliance.yaml` by marking several auth and realtime
features as `implemented` where the Dart implementation is functionally
equivalent to supabase-js, with idiomatic differences aside. This is a
matrix-only change; no library code is touched.

## Changed entries

- `auth.sign_in.sign_in_with_password` (note dropped): weak passwords
surface as a thrown `AuthWeakPasswordException`, the idiomatic Dart
error-handling shape, rather than a `weakPassword` field on the
response.
- `auth.sign_in.sign_in_with_sso` (note kept): exposed as
`getSSOSignInUrl` returning `Future<String>` (the URL) rather than a
structured `{url}` response. Functionally equivalent.
- `auth.sign_in.reauthenticate` (note dropped): neither SDK returns a
session here. The `/reauthenticate` endpoint only sends a
reauthentication OTP and returns no session, and supabase-js's
`reauthenticate` returns an `AuthResponse` whose `user` and `session`
are always null, so the `Future<void>` return is equivalent.
- `auth.session.sign_out` (note dropped): marked implemented.
- `auth.identities.link_identity` (note kept): split into
`getLinkIdentityUrl` (OAuth) and `linkIdentityWithIdToken` (ID token)
rather than a single overloaded method.
- `auth.identities.list_identities` (note dropped): `getUserIdentities`
returns a `List<UserIdentity>` directly, the idiomatic Dart shape,
rather than a `{data, error}` object.
- `realtime.channel.broadcast_http` (note kept): `httpSend` delivers
broadcasts over REST and is functionally equivalent.

## Follow-up (v3)

Two entries above keep a note describing an idiomatic difference that
would be a breaking change to fully align, each tracked for v3:

- Aligning the SSO API name and return shape with supabase-js: #1598.
- Removing the deprecated automatic REST fallback in
`RealtimeChannel.send()`: #1600.

Both are added to the v3 umbrella #1278.
…es (#1595)

## What

Fills genuine, server-free unit coverage gaps found by measuring actual
line coverage (`dart test --coverage` + `format_coverage`) across the
self-contained packages.

### `supabase_common` → now 100% line coverage on every file
- `test/supabase_common_test.dart`:
  - `FetchOptions` group (defaults + provided values, previously 0%).
- `platform info` group covering the `dart:io` getters
(`platform_io.dart`, previously 0%).
- Extra `ReplaySubject` cases: async error replay, sync value replay,
setter-assigned `onListen`/`onCancel`, and the `onPause`/`onResume`
no-ops.
- `test/retry_test.dart`: covers the `onRetry` callback (the one
uncovered line in `retry.dart`).

### `storage_client/lib/src/types.dart` → 86.8% to ~99%
New `test/types_test.dart` (29 tests):
`Bucket`/`FileObject`/`FileObjectV2`/`PaginatedListResult` `fromJson`
(including the `FormatException` path and defaults), the
`toMap`/`toQueryParameters` serializers, `SignedUrl`
equality/`hashCode`/`copyWith`/`toString`, exhaustive `switch` matching
over the sealed `SignedUrlResult`, `StorageException`,
`TransformOptions.toQueryParams`, `DownloadBehavior` and
`StorageRetryController`.

All new and modified tests pass and are formatted. No production code
changed.

## Issues surfaced while writing these tests

1. **Dead defensive branch in `storage_client/lib/src/fetch.dart`** —
`_handleError`'s `else` branch (handling a non-`http.Response` error) is
unreachable; both call sites only ever pass an `http.Response`. Left
as-is here; flagging for follow-up.
2. **`ReplaySubject` sync-replay quirk** — a `sync: true` subject
delivers live events synchronously but replays the latest buffered event
to a late subscriber asynchronously (because `Stream.multi` schedules
its generator). Tests await accordingly.
3. **Pre-existing integration failures (unrelated to this PR)** —
`storage_client/test/client_test.dart` and 62 postgrest "Default http
client" tests require a running/clean local server; the additions here
are all offline/mock-based.
…passkey enrollment (#1603)

## Summary

Implements the WebAuthn MFA enrollment `friendlyName` and `user.name`
fallback from supabase-js in the Dart passkey API, reopening SDK-701
(previously resolved as not_applicable in #1556).

`GoTruePasskeyApi.startRegistration()` now accepts an optional
`friendlyName`. Between fetching the registration options and the
platform ceremony, it backfills `options['user']['name']` (and
`displayName`) when the server omits it, using `friendlyName` if given
or a generic default (`Passkey`) otherwise. This mirrors supabase-js's
pre-`navigator.credentials.create()` patch, expressed idiomatically
without the `${user.id}:` concatenation.
`GoTrueClientPasskey.registerPasskey()` in `supabase_flutter` threads
`friendlyName` through to the full ceremony.

## Verify first (server behavior)

Confirmed against `supabase/auth`: `webAuthnUser.WebAuthnName()` returns
the user's email or phone, and an empty string when the user has
neither. The server therefore *can* omit a meaningful `user.name`, so
the fallback is load-bearing, not merely defensive.

## No passkeys library change needed

The `passkeys` plugin's `UserType` requires non-null
`name`/`displayName`. Because the fallback runs in `gotrue` before the
options reach the plugin, the plugin always receives a valid name and
needs no change.

## Outcome: implemented

## Tests

- `packages/gotrue/test/passkey_test.dart`: enrollment with
`friendlyName`, without `friendlyName` (default), and the
server-omits-`user.name` case; plus a guard that a server-provided
`user.name` is preserved. All 20 passkey tests pass.

## Compliance matrix

`auth.mfa.enroll` (partially_implemented): dropped the "WebAuthn has no
Dart equivalent" caveat; the note now points to
`auth.passkey.register_passkey`, which accepts a `friendlyName` and
applies the same `user.name` fallback. No new public symbols (optional
named parameters only).

## Reference

supabase-js PR #1763, commit f99a58c.

Ref: SDK-701
#1601)

## Summary

Under the PKCE flow, `updateUser({ email })` now generates a PKCE code
verifier and sends the corresponding `code_challenge` /
`code_challenge_method` in the request body, mirroring what `resend()`
and `getSSOSignInUrl` already do. This lets an email change started
under PKCE complete via `exchangeCodeForSession()`.

The fields are only added when an email is being changed, so no public
API surface or return type changes. Non-PKCE flows are unaffected (the
challenge is `null`).

## Outcome

implemented

## Reference

- supabase-js parity, following the already-shipped Dart `resend()` PKCE
work (SDK-1023).

## Compliance matrix

- `auth.session.update_user` → `implemented` (was
`partially_implemented`).

## Tests

- Added mock tests in `packages/gotrue/test/otp_mock_test.dart`: PKCE
email change includes the code challenge, non-email updates omit it, and
implicit flow omits it.

Closes SDK-1318
…1593)

## Summary

Adds request cancellation to `FunctionsClient.invoke`, closing the gap
versus the canonical compliance matrix. `invoke` now accepts an optional
`abortSignal` Future; completing it cancels the in-flight HTTP request
and throws `RequestAbortedException`, mirroring how the database client
exposes cancellation through `PostgrestBuilder.abortSignal`.

This is done idiomatically with the `http` package's
`AbortableRequest`/`AbortableMultipartRequest` (wiring `abortTrigger`),
so it works for both regular and multipart invocations.
`RequestAbortedException` is re-exported from the package so callers can
catch it.

A per-invocation timeout is now expressible as `invoke(..., abortSignal:
Future.delayed(duration))`, so the previously `not_applicable`
`functions.invocation.timeout` note is updated accordingly.

## Outcome

**implemented**

## Reference

Mirrors the existing Dart
`database.using_modifiers.request_cancellation` idiom
(`PostgrestBuilder.abortSignal`).

## Compliance matrix

- `functions.invocation.request_cancellation` → **implemented** (symbol
`FunctionsClient.invoke` registered)

## Tests

- Aborting an in-flight request throws `RequestAbortedException`
- Aborting a multipart request throws `RequestAbortedException`
- A never-firing signal lets the request complete normally

SDK-1310
## Summary

Adds support for analytics (Apache Iceberg) buckets to `storage_client`,
covering the bucket lifecycle: create, list, and delete. These map to
the storage server's Iceberg bucket endpoints under `/iceberg/bucket`.

New public API on the storage bucket client:
- `createAnalyticsBucket(String id)` → `AnalyticsBucket` — `POST
/iceberg/bucket`
- `listAnalyticsBuckets([ListBucketsOptions options])` →
`List<AnalyticsBucket>` — `GET /iceberg/bucket`
- `deleteAnalyticsBucket(String id)` → `String` — `DELETE
/iceberg/bucket/:name`

Listing reuses the existing `ListBucketsOptions`
(limit/offset/search/sortColumn/sortOrder), matching the file bucket
listing API. A new `AnalyticsBucket` type models the response (`id`,
`name`, `createdAt`, `updatedAt`).

## Scope

This PR covers the analytics **bucket** capabilities only. The Iceberg
REST Catalog operations (namespaces and tables) are a much larger
surface (a standalone `iceberg-js`-equivalent client) and are tracked as
a follow-up in SDK-1309.

## Outcome

`implemented` for:
- `storage.analytics.create_analytics_bucket`
- `storage.analytics.list_analytics_buckets`
- `storage.analytics.delete_analytics_bucket`

## Tests

Added unit tests in `basic_test.dart` (mock HTTP client) verifying
request method/path/body and response parsing for all three methods.
Full `storage_client` mock test suite passes.

## Reference

Supabase Storage server Iceberg bucket routes
(`src/http/routes/iceberg/bucket.ts`). In supabase-js the analytics
bucket API is not yet in the storage client; the Iceberg catalog lives
in the standalone `supabase/iceberg-js` package.

## Compliance matrix

`sdk-compliance.yaml`: the three capabilities above → `implemented`,
with new public symbols registered.

Part of SDK-1308.
Prepared all packages to be released to pub.dev

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
## What

Adds `pull-requests: write` to the `SDK Compliance` workflow (both the
top-level `permissions` and the `validate` job) so the reusable workflow
it calls can run.

## Why

Every PR run of `SDK Compliance` has failed with `startup_failure` since
2026-07-15. It is not caused by any individual PR.

The workflow calls the `@main`-pinned reusable workflow
`supabase/sdk/.github/workflows/validate-sdk-compliance-dart.yml`. In
supabase/sdk #60 (commit `45dba43`), the reusable workflow's `check` job
started requesting `pull-requests: write` so it can post the capability
matrix drift comment.

A called reusable workflow's `GITHUB_TOKEN` permissions can only be
equal to or more restrictive than the caller's. This caller only granted
`contents: read`, so the reusable workflow requesting a scope the caller
does not have makes GitHub reject the run at compile time, before any
job starts.

Granting `pull-requests: write` in the caller lets the token be passed
down so the drift comment step can post.

## Notes

- The same fix is needed in the other SDK repos that consume this
reusable workflow (e.g. supabase-js), which are failing the same way.
- The drift check posts/updates a sticky PR comment; it is best-effort
and swallows 403s on fork PRs.
…ell values (#1612)

## What kind of change does this PR introduce?

Bug fix (internal exception noise; no observable behavior change).

## What is the current behavior?

`convertCell`'s doc comment says *"If the value of the cell is `null`,
returns null"* — but the code never checks for null. A null value in an
`int2/int4/int8/oid` (or `float4/float8/numeric`) column falls through
to `toInt`/`toDouble`, which do:

```dart
try {
  return int.parse(value.toString()); // null → "null" → FormatException
} catch (_) {
  return null;
}
```

So every `postgres_changes` payload containing a null numeric column
throws (and immediately catches) `FormatException: Invalid radix-10
number (at character 1) null` inside the library.

The converted result is correct, but the exception-as-control-flow fires
on **every CDC frame** for any subscribed table with nullable numeric
columns. In practice this means anyone debugging a realtime Flutter app
with "break on all exceptions" enabled gets stopped in
`integers_patch.dart` constantly, with no way to silence it from app
code.

For reference, realtime-js doesn't have this problem: its `toNumber`
only parses `typeof value === 'string'` and passes null through
untouched.

## What is the new behavior?

- `convertCell` short-circuits `null` → `null`, matching its own
documented behavior and realtime-js
- `toInt`/`toDouble` use `tryParse` instead of `parse` inside try/catch,
so unparseable strings also no longer throw internally

Output is unchanged for every input — existing tests already pin
`convertCell('int8', null) => null` and `convertCell('float8', null) =>
null`, and they still pass. Added direct unit tests for
`toInt`/`toDouble` covering null, empty, and non-numeric inputs.

## Additional context

Repro: subscribe to `postgres_changes` on any table with a nullable
`integer` column, update a row while the column is null, run with a
debugger set to break on all exceptions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ject (#1607)

## Summary

Adds `StorageFileApi.purgeCache(path, {transformations})` to invalidate
the CDN cache for a single object in a bucket. Issues `DELETE
/cdn/{bucket}/{path}` against the storage base URL and returns the
server's success message. When `transformations` is `true`, only the
resized/formatted variants are purged, leaving the original cached
object intact; otherwise the object cache is purged.

Requires the service-role key and the tenant `purgeCache` feature
enabled on the storage server.

**Outcome:** implemented

**Reference:** supabase-js PR
[#2429](supabase/supabase-js#2429)
(`storage.file_buckets.purge_cache`)

## Compliance matrix

- `storage.file_buckets.purge_cache` → `implemented` (symbol
`StorageFileApi.purgeCache`)

## Tests

Added unit tests in `packages/storage_client/test/basic_test.dart`
covering the DELETE endpoint/URL and the `transformations` query
parameter.

SDK-1331
#1613)

## What

Adds the backing public symbol `GoTrueClient.getSSOSignInUrl` to the
`auth.sign_in.sign_in_with_sso` entry in `sdk-compliance.yaml`.

## Why

SSO sign-in is already functionally complete in the Dart SDK: the
redirect URL is obtained via the public `getSSOSignInUrl`, which is
re-exported all the way up so end users can call
`supabase.auth.getSSOSignInUrl(...)` directly. The matrix already
documented the deviation from supabase-js (returns `Future<String>`
instead of the structured `{ url }` response); this change records the
concrete symbol that backs the capability, matching how other entries
(e.g. `auth.session.get_session` → `GoTrueClient.getSession`) list
theirs.

Follow-up to the discussion on #1598, which was closed as not planned
since the capability is already reachable and the name/return-shape
alignment is a v3 concern.
…lities (#1614)

## What

Adds a `symbols:` list to every capability in `sdk-compliance.yaml` that
was marked `implemented` but had no registered symbols (156 capabilities
across auth, database, storage, realtime, functions, and client).

> **Stacked on #1613.** This PR targets `chore/sso-compliance-symbol`
and inherits the `sign_in_with_sso` symbol registered there;
review/merge #1613 first, then this.

## Why

The reusable **SDK Compliance** workflow runs a capability-matrix drift
check on every PR (see the drift comment on #1613). For each
`implemented` capability it expects a `symbols:` list so the Dart symbol
extractor can confirm the backing public API actually exists.
Capabilities without a list are reported as unverifiable, which is why
every PR got a ~150-line drift warning. This registers the backing
symbol(s) for each remaining one, clearing the drift.

## How

- Ran the same `dart_symbol_extractor` the CI uses (from `supabase/sdk`)
against this repo to produce the ground-truth public symbol dump, then
mapped each capability to the public Dart symbol(s) that implement it
(e.g. `auth.sign_in.sign_up` → `GoTrueClient.signUp`,
`database.using_filters.eq` → `PostgrestFilterBuilder.eq`).
- Every registered symbol was validated to exist in the extractor
output, and the drift-check logic was replicated locally: it now reports
**0 findings**.
- Symbols that are intentionally `@internal` (e.g.
`RealtimeClient.connect`, which is not part of the public parity surface
since Dart connects lazily on subscribe) were not registered; those
capabilities point at the public symbol that actually drives them.

## Notes

- No source or public API changed, this only touches the compliance
matrix, so the "new public symbols" check is unaffected.
…cognized sb_ key subtype (#1615)

## Summary

Parity with supabase-js and supabase-swift
([#1130](supabase/supabase-swift#1130)) for the
new Supabase API key format (`sb_publishable_…` / `sb_secret_…`). These
keys are not JWTs and must never be sent as an `Authorization: Bearer`
token.

- **Functions never sends a new-format key as Bearer.** The
`Authorization: Bearer` header for Functions (and REST/Storage) is
injected per request by the shared `AuthHttpClient`. When no user
session exists it previously fell back to the API key, so a
`sb_publishable_`/`sb_secret_` key was sent as `Bearer …`, which the
Edge Functions runtime rejects. The Functions client now uses a
dedicated `AuthHttpClient` that omits the Bearer header for a new-format
key when there is no session. A genuine session JWT is still sent
normally.
- **Warn on unrecognized `sb_` subtypes.** `SupabaseClient` now warns
once per unrecognized `sb_`-prefixed key subtype at construction. It
never throws (the server, not the SDK, decides key validity) and never
logs the key value.

Scope matches the reference implementations: this affects Functions
only. Legacy JWT keys, REST, Storage, Auth, and Realtime are unchanged.

## Implementation notes

Unlike supabase-swift/js, supabase-flutter injects the Bearer token
through a **shared** `AuthHttpClient` used by REST, Functions, and
Storage, and `FunctionsClient.setAuth` is never wired up from
`SupabaseClient`. To keep the fix scoped to Functions, a separate
`AuthHttpClient` instance (`omitNewApiKeyAsBearer: true`) is created for
the Functions client instead of changing auth-state handling.

## Changes

- `packages/supabase/lib/src/api_key.dart` (new): `isNewApiKey`
classification and `warnOnUnrecognizedApiKey` warn-once helper.
- `packages/supabase/lib/src/auth_http_client.dart`:
`omitNewApiKeyAsBearer` flag suppressing the Bearer header for
new-format keys with no session.
- `packages/supabase/lib/src/supabase_client.dart`: dedicated Functions
`AuthHttpClient`; warn at construction.
- `packages/supabase/test/api_key_test.dart` (new): classification,
warn-once/no-key-leak, and all Bearer-suppression cases.

## Test plan

- [x] `dart analyze` — clean
- [x] `dart test` (supabase package) — all 91 tests pass, including the
8 new ones
- [x] `dart format` run
## What

Adds a runnable **Database CRUD with PostgREST** example under
`examples/database_crud`, plus the shared schema and seed data it runs
against. Closes SDK-1065.

The example is a small task manager that covers everything
`supabase.from(...)` offers for CRUD:

- **Select** tasks joined to their project (`select('*,
projects(name)')`)
- **Filters**: `eq` (project, completion), `ilike` (title search)
- **Ordering**: `order('priority', ascending:
false).order('created_at')`
- **Insert** returning the created joined row
(`insert().select().single()`)
- **Update** title and completion state (`update().eq()`)
- **Delete** (`delete().eq()`)

All database access lives in `lib/tasks_repository.dart`, kept separate
from the UI so the queries are easy to read and can later be driven from
a full end-to-end integration test.

## Shared config

- `examples/supabase/migrations/20240601000000_crud_example.sql` —
`projects` 1:N `tasks` with a foreign key and index, RLS enabled with
permissive demo policies, and grants so the publishable (anon) key can
read projects and manage tasks without signing in.
- `examples/supabase/seed.sql` — sample projects and tasks.

## Wiring

- Registered `examples/database_crud` in the root workspace
`pubspec.yaml` and the `examples/README.md` table. The launcher
auto-discovers it.

## Verification

- `flutter analyze examples` → no issues; `dart format` clean.
- `supabase db reset` against `examples/` applies the migration and seed
cleanly.
- Exercised the full flow over PostgREST with the publishable key:
joined + ordered + `ilike` select, insert returning the joined row,
update, filtered select, and delete (204) with confirmation.
## What

Adds an end-to-end integration test for the `passkeys` example, matching
the ones added for `realtime_room` and `database_crud`. It drives the
actual app widgets against the local Supabase stack.

## The test

`integration_test/passkeys_test.dart` uses `testWidgets` to:

1. Pump `PasskeyExampleApp`, create an account and land on the passkey
management screen (a fresh account shows "No passkeys yet." and the
"Register a passkey" button).
2. Sign out and assert the sign-in screen returns.
3. Attempt a wrong password and assert an error surfaces instead of
signing in.
4. Sign in with the correct password and assert the signed-in screen
returns.

### Scope

The WebAuthn ceremony itself (`registerPasskey` / `signInWithPasskey`)
drives a platform authenticator prompt (Face ID, Windows Hello, a
security key, ...) that cannot be automated headlessly, so it is
exercised manually per the README rather than in this test. The test
covers everything around the ceremony: account creation, the
passkey-list screen, session management and password sign-in error
handling.

A small `_pumpUntil` helper polls the widget tree (auth calls are async
over the network, so the UI can't be `pumpAndSettle`d). Each run uses a
unique email so sign up never collides with an existing user.

Also adds the `integration_test` dev dependency and an "Integration
test" section to the example README.

## Testing

- `flutter analyze`: no issues; `dart format`: clean.
- Ran end to end against a local stack, twice, both green. Integration
tests need a device, so run it with `-d macos`, an emulator or a real
device.
)

## What

Adds a full end-to-end integration test for the `database_crud` example,
matching the one added for `realtime_room`. It drives the actual app
widgets against the local Supabase stack rather than calling the
repository directly.

## The test

`integration_test/tasks_test.dart` uses `testWidgets` to:

1. Pump `CrudExampleApp` and wait for the seeded tasks to load (select).
2. **Filter** by title through the search field and assert the list
narrows, then clears (ilike).
3. **Create** a task through the new-task dialog and assert it appears
(insert).
4. **Complete** it by ticking the checkbox in its tile and assert the
checkbox becomes checked (update).
5. **Rename** it through the edit dialog and assert the new title
replaces the old (update).
6. **Delete** it and assert it leaves the list (delete).

Because reads and writes go over the network, the UI can't be settled
with `pumpAndSettle`; a small `_pumpUntil` helper polls the widget tree
with a timeout instead. The test uses a unique task title and a
`tearDown` that removes any `E2E %` rows, so it runs repeatably and
leaves the seed data untouched.

Also adds the `integration_test` dev dependency and an "Integration
test" section to the example README.

## Testing

- `flutter analyze`: no issues; `dart format`: clean.
- Ran end to end against a local stack, twice, both green. Integration
tests need a device, so run it with `-d macos`, an emulator or a real
device.
)

## What

Adds a weekly workflow that keeps `sdk-compliance.yaml` in sync with new
capability IDs added to the canonical spec at
https://github.com/supabase/sdk.

The sync logic lives centrally in `supabase/sdk` as a reusable workflow
(see supabase/sdk#65), mirroring the existing
`validate-sdk-compliance-dart.yml` pattern. This repo just adds a thin
caller:

- **`.github/workflows/sync-compliance.yml`** — runs Mondays 06:00 UTC
(and on manual dispatch), calls
`supabase/sdk/.github/workflows/sync-sdk-compliance.yml@main`, passing
this repo's `APP_ID`/`PRIVATE_KEY` app-token secrets so the sync PR is
opened under our own token.

The reusable workflow diffs the canonical feature IDs against the keys
already in `sdk-compliance.yaml`, inserts any missing ones as
`not_implemented` next to their `area.group.` siblings (correct section,
no guessing), and opens/updates a PR here.

## Why centralized

The diff/insert logic is language-agnostic, so it belongs in
`supabase/sdk` where every SDK can share one implementation. Each SDK
repo adds the same thin caller and runs on its own token, so no
cross-repo permissions are needed.

## Notes

- Depends on supabase/sdk#65 merging first (the `@main` reference
resolves once it lands).
- Only adds new IDs as `not_implemented`; real status and symbols are
triaged by a human / `/parity`.
- Opens a PR rather than committing directly.
#1617)

## Summary

Blocks adding a `postgres_changes` listener after `subscribe()` has been
called, matching the behavior introduced in supabase-js. Calling
`onPostgresChanges(...)` while the channel is `joining` or `joined` now
throws a clear error instead of silently registering a binding that
would never be applied to the join payload.

**Outcome:** implemented

## Details

- `RealtimeChannel.onEvents` now throws when a `postgres_changes`
binding is added while the channel is joining or joined, with the
message `cannot add \`postgres_changes\` callbacks for <topic> after
\`subscribe()\`.`
- Presence listeners are intentionally **not** blocked: the Dart SDK
deliberately supports adding presence callbacks after join and
resubscribes with presence enabled (`_handlePresenceUpdate`). Blocking
presence would regress that established Dart idiom, so this differs from
supabase-js by design.

## Reference

- supabase-js commit `219c0c16`, PR supabase/supabase-js#2201

## Compliance matrix

`realtime.subscriptions.postgres_changes` was already `implemented`;
this change makes the behavior equivalent for the post-subscribe case.
No new public symbols, so no matrix edit was required.

Linear: SDK-827
)

## Summary

The binary broadcast serializer measured header field lengths with
`String.length` (UTF-16 code units) and wrote one byte per code unit.
Any multi-byte character (accents, emoji, non-Latin scripts) in the
topic, ref, join-ref, event name, or metadata corrupted the length
prefixes and desynchronized the binary frame. Each header field is now
encoded to UTF-8 bytes first, and the byte length is used for both the
length prefixes and the write offsets, matching the `utf8.decode` on the
decode side.

**Outcome:** implemented (bug fix)

## Changes
- `packages/realtime_client/lib/src/serializer.dart` — UTF-8 encode
`topic`, `ref`, `joinRef`, `userEvent`, and `metadata` before
measuring/writing; replaced the code-unit `_writeString` helper with a
byte-copying `_writeBytes`.
- `packages/realtime_client/test/serializer_test.dart` — added a test
covering multi-byte header fields (accented text and emoji)
round-tripping correctly.

## Reference
supabase-js PR #2516 / commit `b4ddbd5` (fix(realtime): encode broadcast
header fields as UTF-8)

## Compliance matrix
`realtime.configuration.binary_protocol` remains `implemented` (no new
public symbols; the change is internal to the non-exported serializer).

Closes SDK-1322
…#1624)

## What

Adds a new `supabase_flutter` example under
`examples/storage_transforms` that demonstrates Supabase Storage:
uploading images, listing them, serving them through image
transformations, downloading transformed bytes and deleting objects.

Closes SDK-1067.

## The example

A small image gallery:

- **Upload** generated PNG bytes to a public bucket (`uploadBinary`).
- **List** the objects, newest first (`list`).
- **Transform** images on the fly with `TransformOptions` (width,
height, resize mode, quality) via `getPublicUrl`, so the grid loads a
small cropped thumbnail and the detail view renders several resized
variants.
- **Download** transformed bytes (`download`).
- **Delete** an object (`remove`).

All Storage access lives in `lib/storage_repository.dart`, kept separate
from the UI so it is easy to read and to drive from an integration test.
Sample images are drawn in memory in `lib/sample_image.dart`, so the
example uploads real image bytes without bundling asset files or
depending on an image picker.

## Backend wiring

- New migration
`examples/supabase/migrations/20240603000000_storage_transforms_example.sql`
creates a public `images` bucket with row level security policies
allowing the demo to view, upload, overwrite and delete objects with the
publishable key.
- `examples/supabase/config.toml` enables
`[storage.image_transformation]`.
- Registered `examples/storage_transforms` in the root workspace and the
`examples/README.md` table.

## Testing

Verified end to end against the local stack (`supabase db reset`, then
upload/list/transform-render/authenticated-transformed-download/delete
over HTTP): uploads succeed, the public render endpoint returns a
resized image, and deletes clean up. `dart format` and `flutter analyze`
are clean.

The end-to-end integration test is a follow-up stacked PR, matching the
other examples.
…xample (#1625)

Stacked on #1624.

Adds `examples/storage_transforms/integration_test/storage_test.dart`,
an end-to-end test that runs against the local Supabase stack, matching
the other examples' e2e tests.

It covers:

- **Repository flow:** upload generated PNG bytes, confirm the object
appears in `list`, build a transformed public URL and check it targets
the render endpoint with the transform query parameters, download the
original and a transformed variant and decode both to assert the
transform actually resized the image (640×640 → 100×100), then delete
and confirm it is gone.
- **Widget flow:** drive the app to upload an image through the FAB,
open the detail view and see the transformation variants by name, then
delete from the detail view and land back on an empty gallery.

Also adds the `integration_test` dev dependency and a "Integration test"
section to the example README.

## Running

With the local stack up:

```bash
cd examples/storage_transforms
flutter test integration_test/storage_test.dart -d macos \
  --dart-define=SUPABASE_URL=http://localhost:54321 \
  --dart-define=SUPABASE_PUBLISHABLE_KEY=YOUR_LOCAL_PUBLISHABLE_KEY
```
Pins the `sync-compliance.yml` caller's reference to the reusable
workflow at supabase/sdk from `@main` to the current main SHA
(`faba359a57d270bbbf68fd6b583db0604ddce8af`), matching the pinning being
applied across the other SDK caller repos (supabase-js #2534,
supabase-py #1541, supabase-swift #1128).

The original caller workflow was merged in #1606 with a floating `@main`
reference, so this follow-up pins it. The `# main` trailing comment
keeps the tracked branch visible for future bumps.
## What

Adds a repo-level `.coderabbit.yaml` that disables CodeRabbit's failing
commit status:

```yaml
reviews:
  commit_status: true
  fail_commit_status: false
```

## Why

Open PRs are currently showing a red `CodeRabbit` check with the reason
**"Review rate limited"** (e.g. #1630, #1629, #1627), even though
nothing is actually wrong with the change. A completed review shows
`pass` (e.g. #1626).

- `commit_status: true` keeps the useful passing check for completed
reviews.
- `fail_commit_status: false` stops CodeRabbit from setting a `failure`
status when it can't review (which is what the rate-limit case
triggers).

Committing the config overrides CodeRabbit's cloud/org defaults.

## Note

If a rate-limited PR still goes red after this lands, the rate-limit
failure path ignores this flag and the only remaining lever is
`commit_status: false` (disables the check entirely, losing the green
pass too).
## Summary

`RealtimeChannel.track()` only forwarded `timeout` to the underlying
`send()`, rebuilding the opts map as `{'timeout': opts['timeout'] ??
_timeout}` and silently dropping any other caller-supplied options. It
now forwards the full `opts` map, matching `untrack()` and the
supabase-js behavior. The timeout fallback is preserved because `send()`
already resolves `opts['timeout'] ?? _timeout`.

**Outcome:** implemented (bug-fix)

## Tests

Added a `track opts forwarding` group in `channel_test.dart` verifying
that a custom non-timeout opt, a custom timeout opt, and the empty
default all reach `send()` unchanged. Full `channel_test.dart` passes
(57 tests).

## References

- Linear: SDK-1321
- supabase-js: PR #2490 / commit `7fd2bc39`

## Compliance matrix

`realtime.presence.track` was already `implemented` with symbol
`RealtimeChannel.track`; this fix corrects its behavior without adding
new public symbols, so no matrix change was required.
## What

Adds an `edge_functions` example under `examples/`, following the same
structure as `database_crud` and `storage_transforms`. It demonstrates
invoking Supabase Edge Functions with `supabase.functions.invoke(...)`:

- **JSON body over POST**, reading the JSON response back
(`invoke('greet', body: {...})`).
- **GET with query parameters** instead of a body (`method:
HttpMethod.get, queryParameters: {...}`).
- **Custom headers** sent to the function and echoed back (`headers:
{...}`).
- **Plain text in and out**: a `String` body is sent as `text/plain` and
the `text/plain` response arrives as a `String` (`invoke('shout', body:
text)`).
- **Error handling**: a non-2xx response throws a `FunctionException`
whose `details` carry the response body (`invoke('word-count', ...)`).

## How it's structured

- All Edge Function access lives in `lib/functions_repository.dart`
(`FunctionsRepository`), kept separate from the UI so the calls are easy
to read and to drive from tests. Models with `factory fromJson` live in
`lib/models.dart`. The Material 3 UI in `lib/main.dart` stays thin.
- Three demo functions ship under `examples/supabase/functions/`:
`greet`, `shout` and `word-count`.
- The Edge Runtime is enabled in `examples/supabase/config.toml`;
`supabase start` serves the functions automatically. The demo functions
set `verify_jwt = false` so the example can call them with just the
publishable key, matching the other examples which run unauthenticated.
- The new package is registered in the workspace `pubspec.yaml` and
listed in `examples/README.md`.

## Testing

- `dart format .` and `flutter analyze` are clean.
- `integration_test/functions_test.dart` was run against the local stack
(`supabase start`) on web (Chrome 150) via `flutter drive`: **all tests
passed**. It covers the repository flow (JSON greeting over POST and
GET, plain-text transform, and a 400 validation error surfacing as a
`FunctionException`) plus a UI smoke test.

A stacked PR (#1633) adds a full end-to-end suite covering the entire
`functions.invoke` surface.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
  - Added a Flutter example demonstrating Supabase Edge Functions.
- Added greeting, plain-text transformation, and word-count function
examples.
- Added support for POST requests, query parameters, custom headers, and
error handling.
- Enabled the example to run in the browser and against the local stack.

- **Documentation**
- Added setup, usage, project structure, and integration testing
instructions.

- **Tests**
- Added end-to-end coverage for function responses, validation errors,
and UI interactions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ession

# Conflicts:
#	packages/supabase_flutter/README.md
#	packages/supabase_flutter/lib/src/supabase_auth.dart
#	packages/supabase_flutter/pubspec.yaml
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 351b3b3c-3d73-4b46-9c2f-6d7ae004fb22

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants