-
Notifications
You must be signed in to change notification settings - Fork 468
Description
Summary
When using WorkOS AuthKit with Convex in a Next.js App Router project, the Convex React client sometimes enters a permanent unauthenticated state after the access token expires. Even though AuthKit refreshes the token and the Convex backend begins accepting it again, useConvexAuth() remains stuck at:
{ isLoading: false, isAuthenticated: false }
This prevents any UI gated by Convex auth from recovering without a full page reload.
Expected Behavior
When AuthKit refreshes the token and the next Convex request succeeds, useConvexAuth() should flip back to:
{ isLoading: false, isAuthenticated: true }
Actual Behavior
- Token expires -> first reactive queries throw (expected).
- AuthKit automatically refreshes token.
- After the expiry burst, I again see successful authenticated queries for this same user immediately afterwards in the Convex dashboard (same deployment, same client), so the backend is clearly accepting a (refreshed) token for that user again.
- But:
useConvexAuth()never returnsisAuthenticated: trueagain.
The client stays permanently unauthenticated.
Important context: The error responses are thrown by my own Convex functions, not by Convex infrastructure itself. In my queries/mutations I call await auth.getIdentity() and explicitly throw new ConvexError({ code: 401, message: "Unauthorized" }) when identity is null. Convex simply forwards these errors to the client; the bug is about the React auth state not recovering after these user-thrown errors when AuthKit has already refreshed the token.
Minimal Reproduction
1. Convex Client Provider (copy from the docs)
'use client';
import { ReactNode, useCallback, useRef } from 'react';
import { ConvexReactClient } from 'convex/react';
import { ConvexProviderWithAuth } from 'convex/react';
import { AuthKitProvider, useAuth, useAccessToken } from '@workos-inc/authkit-nextjs/components';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<AuthKitProvider>
<ConvexProviderWithAuth client={convex} useAuth={useAuthFromAuthKit}>
{children}
</ConvexProviderWithAuth>
</AuthKitProvider>
);
}
function useAuthFromAuthKit() {
const { user, loading: isLoading } = useAuth();
const { accessToken, loading: tokenLoading, error: tokenError } = useAccessToken();
const loading = (isLoading ?? false) || (tokenLoading ?? false);
const authenticated = !!user && !!accessToken && !loading;
const stableAccessToken = useRef<string | null>(null);
if (accessToken && !tokenError) {
stableAccessToken.current = accessToken;
}
const fetchAccessToken = useCallback(async () => {
if (stableAccessToken.current && !tokenError) {
return stableAccessToken.current;
}
return null;
}, [tokenError]);
return {
isLoading: loading,
isAuthenticated: authenticated,
fetchAccessToken,
};
}Wrap the children of the Next.js layout.tsx with this provider.
I also tried other variants using ConvexProviderWithAuthKit from @convex-dev/workos, but all run into the same issue (the underlying impl is very similar to this anyway).
2. Secure Convex Query
export const secureQuery = query(async ({ auth }) => {
const identity = await auth.getIdentity();
if (!identity) throw new Error("Unauthorized");
return "ok";
});3. Client Page
"use client";
import { useConvexAuth, useQuery } from "convex/react";
import { api } from "~/convex/_generated/api";
export default function Page() {
const { isAuthenticated } = useConvexAuth();
const auth = useConvexAuth();
const result = useQuery(api.query.secureQuery, isAuthenticated ? undefined : "skip");
return <pre>{JSON.stringify({ auth, result }, null, 2)}</pre>;
}If you skip the isAuthenticated check you get the same issue as #242 (Auth vs. Authed Query Race Condition)
4. Steps to Reproduce
- Log in via WorkOS.
- Load the page -> shows authenticated state and valid data.
- Wait for token to expire.
- Trigger a reactive Convex query (e.g. modify data in dashboard).
- Observe
errorin Convex logs. - AuthKit refreshes token (confirmed).
- Convex backend starts returning successful responses again for this user (verified via new successful query entries in the Convex dashboard).
- Client never recovers:
useConvexAuth()staysfalseforever.
Notes
Temporary mitigation: increasing the WorkOS token lifetime to 8-12 hours (default is 5) reduces how often this issue occurs, but does not address the underlying Convex auth recovery problem.
This appears related to another Convex auth race where usePreloadedQuery fails to resubscribe before Convex has authenticated (#242). Both cases suggest missing state transitions in Convex's client auth flow.
I kinda still don't believe, that this is a real bug, and think I simply set up something wrong. It seems bonkers that no one ran into this issue before.