Skip to content

feat(auth): remove navigator.locks-based mutex; introduce commit guard + dispose()#2392

Merged
mandarini merged 7 commits into
supabase:masterfrom
Bewinxed:bewinxed/authjs-lockless
Jun 1, 2026
Merged

feat(auth): remove navigator.locks-based mutex; introduce commit guard + dispose()#2392
mandarini merged 7 commits into
supabase:masterfrom
Bewinxed:bewinxed/authjs-lockless

Conversation

@Bewinxed

@Bewinxed Bewinxed commented May 21, 2026

Copy link
Copy Markdown
Contributor

Description

What changed?

GoTrueClient now defaults to lockless coordination. The navigator.locks-based mutex no longer runs by default — most users get the lockless path. Callers who explicitly pass a custom lock (typically React Native processLock or Node multi-process setups) keep the old behavior on an opt-in legacy path so the change is backwards-compatible.

The lockless default uses:

  • refreshingDeferred (already existed) to single-flight in-instance concurrent refreshes.
  • A two-leg commit guard inside _callRefreshToken: (1) snapshots storage before the rotated-token fetch and re-reads after, discarding rotated tokens if a non-null snapshot was cleared between the two reads (typical case: a concurrent signOut ran _removeSession); (2) captures a session-removal epoch counter before _saveSession and re-checks after, so a signOut that interleaves inside _saveSession's storage-write awaits is also caught. Either leg returns { data: null, error: new AuthRefreshDiscardedError() }. _recoverAndRefresh recognises this error and skips its own _removeSession() call so no duplicate SIGNED_OUT event fires.
  • A new exported error class, AuthRefreshDiscardedError (with an isAuthRefreshDiscardedError type guard), is what the commit guard returns when it throws away a successfully-rotated token. Distinct from AuthRetryableFetchError (transient network) and AuthApiError (server rejection). Surfaces through refreshSession() and getSession() results.
  • A new client.auth.dispose() tears down the auto-refresh interval, the visibilitychange listener, the BroadcastChannel, and registered onAuthStateChange subscribers. Idempotent. Designed for React Strict Mode and HMR cleanup hooks. In-flight fetch calls are not aborted — they run to completion.
  • GoTrue handles cross-tab refresh races server-side via the v1 parent-of-active path at internal/tokens/service.go:376-385, so the client doesn't need to coordinate. The current default is v1 (RefreshTokenAlgorithmVersion = 0); v2 (counter-based) is opt-in via RefreshTokenUpgradePercentage and gated rollout. This PR's reasoning relies on v1 behaviour, which is what customers actually run today.

The legacy opt-in path (settings.lock != null):

  • _acquireLock, pendingInLock, lockAcquired, lockAcquireTimeout, and all 14 call-site wrappers are preserved verbatim from master and gated on this.lock != null. The default path is untouched by them.
  • Every gated site is marked // TODO(v3): remove legacy lock path so the eventual v3 cleanup is a mechanical search-and-delete. A separate v3 follow-up tracks that work.
  • The lock and lockAcquireTimeout constructor options are accepted and honored when supplied; both are @deprecated in JSDoc with "Custom locks still work in v2.x for backwards compatibility. Will be removed in v3."
  • navigatorLock, processLock, LockAcquireTimeoutError, NavigatorLockAcquireTimeoutError, ProcessLockAcquireTimeoutError, and internals in lib/locks.ts remain exported. Still @deprecated.

Stale JSDoc referencing the lock on getSession, onAuthStateChange, _useSession, _challengeAndVerify, and _listFactors now matches the new default behaviour. The @deprecated tag on the async onAuthStateChange overload is kept, with its reason updated to point at the one residual reentry hazard (refreshSession called from inside a TOKEN_REFRESHED handler — refreshingDeferred.resolve happens after _notifyAllSubscribers returns).

Why was this change needed?

The shared mutex has caused seven failure classes on the issue tracker for 18+ months. Each earlier patch fixed one symptom and created another:

  • #1962 configurable timeout
  • #2106 steal fallback
  • #2178 steal-cascade guard

#2235 (primitive swap to processLock) was an earlier attempt at moving off navigator.locks and is no longer being actively pursued. Community PRs #2016 and #2019 fixed individual symptoms and were closed waiting on a structural fix.

Failure classes resolved by this PR's default (lockless) path:

  1. Strict Mode / HMR orphan deadlocks. Nothing to orphan on the default path.
  2. iOS Safari INITIAL_SESSION / getSession() hangs with a persisted session (#936 @bugprone). The default path no longer touches navigator.locks.
  3. Sentry flooding with AbortError: Lock broken by another request with the 'steal' option (#2013 @cpannwitz). The { steal: true } fallback that produced these errors is bypassed on the default path.
  4. Subscriber re-entry deadlock (the one at the old GoTrueClient.ts:3824). The cycle needed the lock; a callback that awaits getUser() now just runs on the default path.
  5. Visibility/tick race (#936 @JTBrinkmann). The visibility handler and the auto-refresh tick no longer share a lock to fight over on the default path.
  6. Stale tickers/listeners on un-disposed clients. dispose() cleans them up.
  7. signOut blocked behind in-flight refresh. signOut now runs concurrently with the refresh on the default path, and the two-leg commit guard prevents the refresh from writing rotated tokens after _removeSession() cleared storage — whether the clear happened before the post-fetch snapshot read or inside _saveSession's storage writes.

Callers on the legacy opt-in path (explicit settings.lock) keep the old serialization semantics and the failure modes that come with them. They accepted those when they opted in; they can drop the option to migrate to the lockless default at any time.

Why default lockless: cross-tab races are handled on the server (GoTrue's parent-of-active), in-instance refresh dedup is already handled by refreshingDeferred, and the only job the lock did beyond that was serializing subscriber callbacks — which is the deadlock we're fixing.

Closes (test target): #2013, #936, #2111

Examples

dispose() in a React app

import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'

export function useSupabase() {
  useEffect(() => {
    const supabase = createClient(URL, KEY)
    return () => {
      supabase.auth.dispose()
    }
  }, [])
}

Subscriber callbacks can call other auth methods now (default path)

// Previously: deadlocked. Callback held the lock; getUser tried to acquire it.
// Now (default path): works.
supabase.auth.onAuthStateChange(async (event, session) => {
  if (event === 'SIGNED_IN') {
    const { data } = await supabase.auth.getUser()
    // ...
  }
})

One residual hazard remains: calling refreshSession from inside a TOKEN_REFRESHED handler still deadlocks via refreshingDeferred. The async overload of onAuthStateChange keeps its @deprecated marker for that reason.

AuthRefreshDiscardedError for the signOut-during-refresh race

import { isAuthRefreshDiscardedError } from '@supabase/auth-js'

const { data, error } = await supabase.auth.refreshSession()
if (isAuthRefreshDiscardedError(error)) {
  // A concurrent signOut cleared storage between when this refresh
  // started and when it came back. The rotated tokens were discarded.
  // The SIGNED_OUT event already fired from the concurrent signOut.
}

Legacy lock opt-in (existing custom-lock users)

// React Native processLock setup or Node multi-process setup
import { processLock } from '@supabase/auth-js'

const supabase = createClient(URL, KEY, {
  auth: { lock: processLock }, // legacy path, still works, deprecated for v3
})

Breaking changes

  • This PR contains no breaking changes

The earlier draft of this PR silently dropped custom lock functions, which was behaviorally breaking for opt-in users. The current design preserves that behavior on a gated legacy path so the change ships as a v2 minor. Custom-lock users have time to migrate before v3 removes the option entirely.

Behaviour changes worth flagging for review

  • Default coordination is lockless. A client constructed without a lock option no longer acquires navigator.locks or any in-process lock. For most users this is invisible (the lock was internal coordination). For users who depended on observing the lock indirectly (rare), the change is observable.
  • _autoRefreshTokenTick may now run concurrently with signOut / setSession / getUser on the default path. Previously _acquireLock(0, ...) made the tick skip whenever any auth op held the lock. The lockless equivalent only skips when refreshingDeferred is set. The commit guard keeps storage consistent under the new concurrency. The legacy lock opt-in path retains the old skip-on-any-lock behavior.
  • Subscriber timing on the default path: subscribers stay awaited; same as before. What changes is that signOut no longer waits for an in-flight refresh's HTTP and continuation to finish before its own fetch goes out. Both fetches now run concurrently, and the commit guard keeps storage consistent.
  • New @deprecated warnings on lock and lockAcquireTimeout. Both options now carry @deprecated JSDoc on @supabase/auth-js and @supabase/supabase-js types. Consumers with strict deprecation lint rules (e.g. deprecation/deprecation as error) will see new warnings after upgrading. The options continue to work in v2.x; removal is slated for v3.

Checklist

  • I have read the Contributing Guidelines
  • My PR title follows the conventional commit format: <type>(<scope>): <description>
  • I have run npx nx format to ensure consistent code formatting
  • I have added tests for new functionality (commit guard, dispose(), subscriber re-entry no-deadlock, default vs. legacy path)
  • I have updated documentation (JSDoc on affected methods, type-level deprecation notices)

Additional notes

Alternatives considered

Continuing to patch navigator.locks (better steal recovery, lower timeouts, smarter error filtering). Each prior patch in this direction (#1962, #2106, #2178) fixed one symptom and produced another. The Web Locks API has no recovery for orphaned holders other than { steal: true }, which leaves the previous fn() running concurrently with the new holder and creates the steal-cascade error storm. Each fix here swaps one failure for another instead of removing the contention that causes them.

Swapping navigatorLock for processLock as the default browser lock (the direction #2235 explored). Removes the cross-process orphan failure but keeps the same shape of the bug: one primitive serializing operations that don't all need the same synchronization.

Non-blocking subscriber notifications alone (#2016). Fixes subscriber re-entry. The other six failure classes still bite.

Removing the lock entirely (no legacy opt-in). The earlier draft of this PR did this. It silently dropped custom locks supplied via settings.lock, which broke React Native processLock and Node multi-process setups. The current design preserves those callers on a gated legacy path so the change is non-breaking; the legacy path is slated for removal in v3.

Adding an AbortController layer for in-flight operation cancellation. Deferred to a follow-up, not included here. The commit guard catches the races that affect correctness (signOut overwriting cleared storage with rotated tokens, in both the fetch and storage-write phases). AbortController would be a UX improvement on top: cancel the in-flight refresh as soon as signOut runs, instead of letting it finish and discarding the result. Out of scope for this PR.

Deferring the commit guard and accepting eventual consistency. Considered and rejected. Without a guard, a refresh that completes after _removeSession() cleared storage will write the rotated tokens back. Subscribers then see SIGNED_OUT followed by TOKEN_REFRESHED, with stale tokens in storage until the next refresh tick (~30s) fails against the server and clears them. The guard is about 30 lines (both legs combined). Better to land it with this PR than to ship the bug and patch it later.

Server-side context

Why cross-tab is safe on the default path: GoTrue's parent-of-active path at internal/tokens/service.go:376-385 (the v1 branch, *models.RefreshToken). When a request arrives with a revoked refresh token whose child is the currently-active token, the server returns the active token instead of rejecting. Two tabs that POST the same refresh token concurrently both receive the same rotated token under DB row locking. This is the production-default behaviour (RefreshTokenAlgorithmVersion = 0, v1). v2 (counter-based, gated on RefreshTokenUpgradePercentage) is safe under the same N-tab concurrency, covered by TestConcurrentReuse.

Test coverage

Updated tests in packages/core/auth-js/test/GoTrueClient.test.ts:

  • 'Lockless coordination (default) and legacy lock opt-in' describe:
    • default path: client with no lock option, assert _acquireLock is never invoked
    • legacy path: client with custom lock, assert lock IS invoked (.toHaveBeenCalled())
    • lockAcquireTimeout accepted on both paths (lockless ignores it at runtime; legacy uses it)
    • subscriber callback may call other auth methods from inside onAuthStateChange without deadlock
  • 'dispose() lifecycle' describe: idempotency, subscriber clearing, ticker stopping.
  • 'Refresh commit guard (signOut-during-refresh race)' describe (4 tests): cleared-mid-flight discard, empty-storage acceptance, different-session acceptance, different-session-written-mid-flight discard.

Updated tests in packages/core/auth-js/test/GoTrueClient.browser.test.ts:

  • 'Lockless coordination: navigator.locks should NOT be invoked': default path no longer touches the browser API.
  • 'Legacy lock opt-in: custom lock function is invoked when supplied': custom lock IS called on the legacy path.

Updated tests in packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts:

  • 'should pass through lockAcquireTimeout option' and 'should accept auth.lockAcquireTimeout and wire it to auth client': assert the option flows from createClient through to the auth client instance (via a targeted as unknown as { lockAcquireTimeout: number } cast). Comment in-source explains the cast is greppable for the v3 cleanup.

AI review responses

Addresses inline AI review feedback on this PR:

  • TOCTOU window inside _saveSession (MEDIUM): closed via a new _sessionRemovalEpoch counter, incremented synchronously at the top of _removeSession before any await, captured in _callRefreshToken immediately before _saveSession, and re-checked after. If the epoch advances during the save, the rotated tokens are undone directly with removeItemAsync (not via _removeSession, which would emit a duplicate SIGNED_OUT).
  • Refresh-token fragments in debug logs (LOW): removed substring(0, 5) of refresh_token from all four _debug() paths (_callRefreshToken / _refreshAccessToken debugName construction, plus the two fields in the commit-guard discard log). Discard logs now use presence indicators ('present' / 'replaced' / 'cleared') instead of credential fragments.

Resolved during review

  1. AbortSignal.any plumbing in lib/fetch.ts — not done here and not blocking. Deferred to the AbortController follow-up, which gates on AbortSignal.any (Node 20.3+ floor or polyfill). Worth noting the follow-up has rate-limit-posture implications, not just UX: cancelling refreshes that the commit guard is about to discard reduces project-level /token volume.
  2. @supabase/supabase-js types deprecation — confirmed. lock and lockAcquireTimeout are @deprecated in both @supabase/auth-js and @supabase/supabase-js types. The supabase-js surface is the one most users see; deprecating only in auth-js would miss the JSDoc tooltip for the passthrough. Flagged separately under "Behaviour changes worth flagging" so the new lint warnings don't surprise downstream consumers.

v3 cleanup

A separate v3 follow-up tracks the mechanical removal of the legacy lock path. Every gated site in GoTrueClient.ts has a // TODO(v3): remove legacy lock path marker; the follow-up lists the full file-by-file scope (covering both @supabase/auth-js and @supabase/supabase-js).

Replaces #2387 (which was opened against develop before the v3 branching update).

@Bewinxed Bewinxed force-pushed the bewinxed/authjs-lockless branch 3 times, most recently from 0b30f2d to 36261d5 Compare May 22, 2026 21:21
@mandarini mandarini force-pushed the bewinxed/authjs-lockless branch from 36261d5 to 026e1ee Compare May 25, 2026 10:58
@pkg-pr-new

pkg-pr-new Bot commented May 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2392

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2392

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2392

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2392

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2392

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2392

commit: 1a98a57

@mandarini mandarini self-assigned this May 25, 2026
Bewinxed and others added 5 commits May 25, 2026 17:42
…guard + dispose()

Removes the `_acquireLock` mutex that wrapped every auth operation and the
underlying `navigator.locks` / `processLock` machinery. Replaces it with two
lighter primitives that target the specific synchronization needs each
operation actually has:

- `refreshingDeferred` (already existed) continues to single-flight the
  refresh path within an instance.
- A storage-level commit guard in `_callRefreshToken` re-reads the storage
  refresh_token between the rotated-tokens response and `_saveSession`.
  If storage changed under us (e.g. a concurrent `signOut` ran
  `_removeSession`), the rotated tokens are discarded rather than written
  back. Returns `AuthRefreshDiscardedError` on the result.

Cross-tab refresh races are handled server-side by GoTrue's v1
parent-of-active mechanism at `internal/tokens/service.go:376-385`, so no
client-side coordination is needed.

New `client.dispose()` tears down the auto-refresh interval, the
`visibilitychange` listener, and the BroadcastChannel; clears registered
`onAuthStateChange` subscribers. Idempotent. Call from cleanup hooks in
React Strict Mode / HMR contexts to prevent stale tickers from outliving
the client.

The `lock` and `lockAcquireTimeout` constructor options are silently
ignored for backwards compatibility; both are marked `@deprecated`.
`navigatorLock`, `processLock`, and the `LockAcquireTimeoutError` family
remain exported from `./lib/locks` for one major version.

Stale lock references in JSDoc on `getSession`, `onAuthStateChange`,
`_challengeAndVerify`, and `_listFactors` updated to match the new model.

Test branch only, not for merge yet. See RFC `lockless_auth_coordination`.

Resolves (test target): supabase#2013, supabase#936, supabase#2111
…ispose()

_useSession: explain that concurrent callers can both reach `__loadSession`
because storage reads are idempotent and the only write path (refresh) is
single-flighted downstream by `refreshingDeferred` in `_callRefreshToken`.
No serialization is needed at this layer.

dispose(): add a lifecycle caveat clarifying that in-flight refreshes are
not aborted, so a disposed instance can still persist a rotated session to
storage after `dispose()` returns. A subsequent `createClient` against the
same `storageKey` will pick that session up. Notes the mitigation (await
pending ops before dispose, or use a fresh `storageKey`).

Doc-only changes; no runtime behaviour change.
The two tests at `SupabaseAuthClient.test.ts:61-77` were asserting that
`lockAcquireTimeout` is stored as a runtime field on the auth client
(`expect((authClient as any).lockAcquireTimeout).toBe(30_000)`). That field
no longer exists after the lockless refactor — the option is accepted by
the type for backwards compatibility but is silently ignored at runtime
because the client doesn't acquire a lock around auth operations.

Rewrite both tests to verify the new contract:
- `_initSupabaseAuthClient` accepts the option without throwing.
- `createClient` accepts it through `auth.lockAcquireTimeout`, the
  auth client is still constructed, but the value is not stored as a
  runtime field (`toBeUndefined()`).

Fixes the CI failure in `Supabase-JS Integration CI / Unit + Type Check`
(2 failed, 102 passed → now 104 passed) on all three platforms.
@mandarini mandarini force-pushed the bewinxed/authjs-lockless branch from 265c2a9 to 3ca0099 Compare May 25, 2026 14:42
@mandarini mandarini marked this pull request as ready for review May 26, 2026 10:18
@mandarini mandarini requested review from a team as code owners May 26, 2026 10:18
@mandarini mandarini added the do-not-merge Do not merge this PR. label May 26, 2026
@mandarini

Copy link
Copy Markdown
Contributor

Do not merge until we ask users to test, and dogfood.

Comment thread packages/core/auth-js/src/GoTrueClient.ts
Comment thread packages/core/auth-js/src/GoTrueClient.ts Outdated
@thomaslarsson

Copy link
Copy Markdown

@mandarini Thank you! PR #2392 looks like it should fix the stuff that actually hung the UI for us — mainly the lock deadlocks (#2344-style) and iOS tabs where getSession() never resolved. Happy to test the beta for that. I will keep my own guards in production and write to log if they are called. Will make it easier to know for sure if the issues still exists for us or not.

A few things we'd still like to see addressed (or at least documented) before we'd feel comfortable dropping our own workarounds:

  1. Refresh storm / no backoff after fatal /token
    When refresh fails with something like refresh_token_not_found, the ~30s ticker can keep trying anyway. We saw single clients doing hundreds of /token calls per hour until we hit project rate limits. refreshingDeferred helps with concurrent calls in one client, but there's no "stop trying for a while" after a hard failure. (For example when clearing refreshTokens from the db - we tried that to force token recreate...)

  2. Cookie scope on sign-out (@supabase/ssr)

    If you've ever changed cookie Domain (we went from host-only to .domain), _removeSession() only clears cookies at the current scope. Stale host-only cookies stick around, the session comes back on the next read, and you're back in the loop. Some guidance or a "clear all scopes" option in @supabase/ssr would help a lot.

  3. Multiple clients per tab

    Lockless coordination is per-instance. Two GoTrueClients (easy to do with plain createClient() from @supabase/supabase-js) means two tickers. We saw interleaved refresh patterns that looked like two tabs, but it was one browser with two clients. A runtime warning or stronger singleton story would be nice.

  4. Observability

    Dashboard shows /token volume but not why. Something like optional diagnostic events (refresh_failed_fatal, refresh_discarded, etc.) would make production debugging much easier. We patched window.fetch to figure some of this out.

None of this is meant to block the lockless refactor - we think that's the right direction. These are the gaps we'd still be covering in the client. Happy to share more detail from our logs if useful once we have them. :)

@mandarini

Copy link
Copy Markdown
Contributor

@thomaslarsson Thanks for the detailed writeup.

1. Refresh storm / no backoff after fatal /token

This is a real gap and a good catch. The good news is the server already returns a well-defined set of fatal codes, all HTTP 400 with a structured code field. The ones the client should treat as "stop ticking, do not retry":

  • refresh_token_not_found
  • refresh_token_already_used
  • session_not_found
  • session_expired
  • user_banned

And 429 over_request_rate_limit for the rate-limit case (worth noting: the rate limiter is per-IP, shared across all /token grant types, and the response does not currently carry a Retry-After header, so the client would pick the backoff itself).

The auto-refresh ticker doesn't key off any of these today; it just retries on the next tick. We'll file a follow-up to add fatal-code recognition + backoff in auth-js. Your "hundreds of /token calls per hour" observation is exactly the failure mode that needs.

2. Cookie scope on sign-out (@supabase/ssr)

removeItem in @supabase/ssr clears at a single scope, whatever cookieOptions.domain is configured to at runtime. Migrating from host-only to .domain leaves the host-only cookies behind, the browser keeps returning both in the Cookie header, and parsers may pick the stale one. There is no read-side fix possible (the browser doesn't expose Domain per cookie to JS), so the fix has to be on the write side.

We're looking at landing this in @supabase/ssr: when cookieOptions.domain is set, also emit a host-only Set-Cookie with Max-Age=0 on removal. That covers the common host-only → parent-domain migration automatically. For the rarer .parent1.parent2 case, a documented migration helper that takes a list of legacy scopes is the right shape. This will follow up on a separate PR on @supabase/ssr.

3. Multiple clients per tab

Fair point. A runtime warning on a second GoTrueClient instantiation in the same realm is cheap and would have caught your case. Worth a follow-up; the only nuance is making it survive HMR cleanly (we don't want to warn on Strict-Mode double-mounts), which is exactly what dispose() in this PR is for.

4. Observability

Two halves:

  • Client-side diagnostic events (refresh_failed_fatal, refresh_discarded, refresh_rate_limited, etc.) — yes, this can live in auth-js as optional listeners alongside onAuthStateChange. Reasonable follow-up.
  • Dashboard surfacing /token failures by reason, not just volume: that part isn't in the SDK repo; it's a Studio/platform ask. You can file it under https://github.com/supabase/supabase.

I am going to release this PR tomorrow as part of our next minor release. Let me know how it works for you if you test it ou!

Again, thank you so much for all the details!

@mandarini mandarini removed the do-not-merge Do not merge this PR. label Jun 1, 2026
@mandarini mandarini merged commit 54ec2b6 into supabase:master Jun 1, 2026
69 of 73 checks passed
@Owez

Owez commented Jun 1, 2026

Copy link
Copy Markdown

Wonderful, thanks for this

mandarini pushed a commit to supabase/supabase that referenced this pull request Jun 2, 2026
This PR updates @supabase/*-js libraries to version 2.107.0.

**Source**: manual

**Changes**:
- Updated @supabase/supabase-js to 2.107.0
- Updated @supabase/auth-js to 2.107.0
- Updated @supabase/realtime-js to 2.107.0
- Updated @supabase/postgest-js to 2.107.0
- Refreshed pnpm-lock.yaml

---

## Release Notes

## v2.107.0

## 2.107.0 (2026-06-02)

### 🚀 Features

- **auth:** remove navigator.locks-based mutex; introduce commit guard +
dispose() ([#2392](supabase/supabase-js#2392))
- **realtime:** allow httpSend to send binary payload
([#2400](supabase/supabase-js#2400))
- **supabase:** update X-Client-Info to structured metadata format
([#2359](supabase/supabase-js#2359))

### 🩹 Fixes

- **auth:** return AuthInvalidJwtError from getClaims for expired JWT
([#2395](supabase/supabase-js#2395))
- **auth:** recognize ?error= redirects in implicit grant gate
([#2407](supabase/supabase-js#2407))
- **auth): revert fix(auth:** encode client-id in oauth requests
([#2383](supabase/supabase-js#2383),
[#2417](supabase/supabase-js#2417))
- **postgrest:** return a structured error for non-JSON body on
successful responses
([#2398](supabase/supabase-js#2398))
- **release:** pin workspace:* sibling deps before JSR publish
([#2418](supabase/supabase-js#2418))
- **release:** publish gotrue-js legacy mirror via pnpm
([#2419](supabase/supabase-js#2419))

### ❤️ Thank You

- Claude Opus 4.7 (1M context)
- Claude Sonnet 4.6
- Eduardo Gurgel
- Guilherme Souza
- Katerina Skroumpelou @mandarini
- Omar Al Matar @Bewinxed
- youcef zr @youcefzemmar
- youcefzemmar

This PR was created automatically.

Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
imor pushed a commit to supabase/supabase that referenced this pull request Jun 3, 2026
This PR updates @supabase/*-js libraries to version 2.107.0.

**Source**: manual

**Changes**:
- Updated @supabase/supabase-js to 2.107.0
- Updated @supabase/auth-js to 2.107.0
- Updated @supabase/realtime-js to 2.107.0
- Updated @supabase/postgest-js to 2.107.0
- Refreshed pnpm-lock.yaml

---

## Release Notes

## v2.107.0

## 2.107.0 (2026-06-02)

### 🚀 Features

- **auth:** remove navigator.locks-based mutex; introduce commit guard +
dispose() ([#2392](supabase/supabase-js#2392))
- **realtime:** allow httpSend to send binary payload
([#2400](supabase/supabase-js#2400))
- **supabase:** update X-Client-Info to structured metadata format
([#2359](supabase/supabase-js#2359))

### 🩹 Fixes

- **auth:** return AuthInvalidJwtError from getClaims for expired JWT
([#2395](supabase/supabase-js#2395))
- **auth:** recognize ?error= redirects in implicit grant gate
([#2407](supabase/supabase-js#2407))
- **auth): revert fix(auth:** encode client-id in oauth requests
([#2383](supabase/supabase-js#2383),
[#2417](supabase/supabase-js#2417))
- **postgrest:** return a structured error for non-JSON body on
successful responses
([#2398](supabase/supabase-js#2398))
- **release:** pin workspace:* sibling deps before JSR publish
([#2418](supabase/supabase-js#2418))
- **release:** publish gotrue-js legacy mirror via pnpm
([#2419](supabase/supabase-js#2419))

### ❤️ Thank You

- Claude Opus 4.7 (1M context)
- Claude Sonnet 4.6
- Eduardo Gurgel
- Guilherme Souza
- Katerina Skroumpelou @mandarini
- Omar Al Matar @Bewinxed
- youcef zr @youcefzemmar
- youcefzemmar

This PR was created automatically.

Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
mandarini pushed a commit to supabase/ssr that referenced this pull request Jun 3, 2026
This PR updates `@supabase/supabase-js` to v2.107.0.

**Source**: manual

---

## Release Notes

## v2.107.0

## 2.107.0 (2026-06-02)

### 🚀 Features

- **auth:** remove navigator.locks-based mutex; introduce commit guard +
dispose() ([#2392](supabase/supabase-js#2392))
- **realtime:** allow httpSend to send binary payload
([#2400](supabase/supabase-js#2400))
- **supabase:** update X-Client-Info to structured metadata format
([#2359](supabase/supabase-js#2359))

### 🩹 Fixes

- **auth:** return AuthInvalidJwtError from getClaims for expired JWT
([#2395](supabase/supabase-js#2395))
- **auth:** recognize ?error= redirects in implicit grant gate
([#2407](supabase/supabase-js#2407))
- **auth): revert fix(auth:** encode client-id in oauth requests
([#2383](supabase/supabase-js#2383),
[#2417](supabase/supabase-js#2417))
- **postgrest:** return a structured error for non-JSON body on
successful responses
([#2398](supabase/supabase-js#2398))
- **release:** pin workspace:* sibling deps before JSR publish
([#2418](supabase/supabase-js#2418))
- **release:** publish gotrue-js legacy mirror via pnpm
([#2419](supabase/supabase-js#2419))

### ❤️ Thank You

- Claude Opus 4.7 (1M context)
- Claude Sonnet 4.6
- Eduardo Gurgel
- Guilherme Souza
- Katerina Skroumpelou @mandarini
- Omar Al Matar @Bewinxed
- youcef zr @youcefzemmar
- youcefzemmar
## v2.106.2

## 2.106.2 (2026-05-25)

### 🩹 Fixes

- **auth:** restore signup user response
([#2391](supabase/supabase-js#2391))
- **misc:** add react-native export condition for Hermes-safe resolution
([#2393](supabase/supabase-js#2393))

### ❤️ Thank You

- Myroslav Hryhschenko @BLOCKMATERIAL
- Vaibhav @7ttp

This PR was created automatically.

Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
mandarini added a commit to supabase/ssr that referenced this pull request Jun 4, 2026
## Summary

Fixes a stuck-session bug for users who change their cookie `Domain` in
production (typically host-only to `.parent.tld`). After such a
migration, `signOut` could not clear the stale host-only cookies, the
browser kept returning both copies, and the session resurrected on the
next read.

Reported on supabase/supabase-js#2392. No read-side fix is possible:
`document.cookie` does not expose `Domain` to JS, so the fix has to be
on the write side.

## What changed

- `removeItem`, `setItem` chunk cleanup, and `applyServerStorage`: when
`cookieOptions.domain` is set, also emit a `Set-Cookie` clear with no
`Domain` attribute. No change when `domain` is not configured.
- New exported helper `clearAuthCookiesAtScopes` for rarer multi-scope
migrations (`.parent1` to `.parent2`, path changes). Idempotent; safe to
over-call (browser ignores clears at scopes the host does not own).

## Behavior

Not breaking. Zero observable change for clients that do not set
`cookieOptions.domain`. For clients that do, the extra `Set-Cookie` is a
no-op unless a stale host-only cookie at the same name actually exists.
Ship as minor (`feat` for the helper dominates the `fix` for the
auto-clear).

## Tests

New `describe` block in `cookies.spec.ts` covering `removeItem`,
`setItem` chunk cleanup, and `applyServerStorage` for both with-domain
and baseline cases. New `clearAuthCookiesAtScopes.spec.ts` covering the
helper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment