Skip to content

Conversation

@nicknisi
Copy link
Member

@nicknisi nicknisi commented Oct 31, 2025

Summary

Fixes intermittent authentication failures after laptop sleep/wake that caused users to be logged out or redirected to sign-in.

Root Cause

Two independent failures triggered by network instability during wake:

  1. Token refresh race: Convex websocket reconnects before network is ready → getAccessToken() throws → returns null → Convex unmounts authenticated components

  2. Visibility handler reload: AuthKit's visibility change handler detects "Failed to fetch" → triggers window.location.reload() → middleware redirects to sign-in

Solution

Three changes to ConvexClientProvider.tsx:

  1. Add onSessionExpired handler - Prevents AuthKit from reloading the page on network errors
  2. Capture token with ref - Preserves access token across state changes
  3. Fallback to cached token - When getAccessToken() throws, falls back to cached token if still valid

This allows graceful degradation: Convex stays authenticated using valid cached tokens while AuthKit's tokenStore retries refresh in the background.

Testing

  • ✅ Set WorkOS token TTL to 1 minute
  • ✅ Tested with 3+ minute sleep cycles
  • ✅ Authentication remains stable across wake events
  • ✅ No page reloads or sign-in redirects
  • ✅ Token refresh happens successfully in background

Resolves intermittent authentication failures after laptop sleep/wake
that caused users to be logged out or redirected to sign-in.

## Root Cause
Two independent failures triggered by network instability during wake:

1. Token refresh race: Convex reconnects before network is ready →
   getAccessToken() throws → returns null → unmounts components

2. Visibility handler: AuthKit detects "Failed to fetch" →
   reloads page → middleware redirects to sign-in

## Solution
- Add onSessionExpired handler to prevent page reload on network errors
- Capture access token with ref to preserve it across state changes
- Fall back to cached token when getAccessToken() throws
- Let AuthKit's tokenStore handle background retries

This allows graceful degradation: Convex stays authenticated using
valid cached tokens while AuthKit retries refresh in the background.

Tested with 1-minute token TTL and 3+ minute sleep cycles.
Copy link
Contributor

@thomasballinger thomasballinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you able to reproduce the issue without this change, for evidence this fixes it? I'd love to know what's happening and to figure out which of these changes we need


return (
<AuthKitProvider>
<AuthKitProvider onSessionExpired={noop}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do, disable some default onSessionExpired behavior? a comment would be helpful

const isAuthenticated = !!user;

// Keep ref updated with latest token
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why update the accessTokenRef in an effect, can you just assign to it when it updates?

if (isTokenValid(cachedToken)) {
console.log('[Convex Auth] Using cached token during network issues');
return cachedToken!;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the main change here is to try to return the last token on error, does the isTokenValid help at all? The Convex client treats an invalid client roughly like a null, both will clear the auth, so I'd think it'd be fine to skip the isTokenValid and keep it simple.

What kinds of errors are we seeing, is it a network error talking to WorkOS, should we retry it?


try {
if (forceRefreshToken) {
return (await refresh()) ?? null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is refresh() not the right way to refresh?

@joepetrillo
Copy link

Any updates regarding this PR? Without following the template, developing this provider would not be very straightforward (at least for me).

There is an outdated version still in the convex docs, which needs an update as well (assuming this version in the template is correct/better?).

@nicknisi @thomasballinger

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants