#expectAuth compatibility with Convex Auth

24 messages · Page 1 of 1 (latest)

honest trellis
#

Migrating from Better Auth to Convex Auth since app is simple and BA integration has too many flaws.

My main issue that at the moment is if I turn on expectAuth, my OAuth hangs - basically nothing happens after triggering signIn("github") , OAuth doesn't go through, no sessions are assigned.

If I turn off expectAuth, everything works as intended however I get UI flashes during OAuth flow.

I assume there's something with Convex Auth preventing out-of-the-box compatibility with expectAuth, like signIn() running a call blocked by expectAuth? Checked dashboard and auth:signIn is never triggered when expectAuth is enabled. @inner fox

inner foxBOT
#

Hi @honest trellis. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You’ve run into a real limitation: expectAuth is still experimental and doesn’t “just work” with all auth flows yet, including Better Auth and some custom integrations. The knowledge sources show the same class of problems you’re seeing, and they also make clear that expectAuth can easily cause hanging / infinite loading when auth isn’t fully wired up.

Key points from the sources:

  • expectAuth holds back all queries, mutations, and actions “until the first auth token can be sent.” [Convex client options; Base client options]
  • Support explicitly warns that expectAuth can cause infinite loading if the app isn’t ready to provide auth immediately; they recommend turning it off when debugging auth issues. [Infinite loading thread; Better Auth + Convex thread]
  • In several integrations (WorkOS, Clerk, Better Auth, TanStack Start), the only reliably working patterns right now are:

For your specific questions:

#
  1. Is there something about Convex Auth that prevents out‑of‑the‑box compatibility with expectAuth?

    The docs and threads don’t explicitly say “Convex Auth + expectAuth is broken,” but they do say:

    • expectAuth is experimental and may change or disappear. [Convex client options]
    • It’s only recommended for pages that “should only be viewed by authenticated clients,” and several support replies treat it as an advanced / fragile option. [Convex client options; Roast thread]
    • In other integrations, they explicitly say “protected routes + expectAuth and client-side are the only methods that work right now,” and they’re still iterating on race conditions between auth and the Convex client. [WorkOS thread 2]

    So: there’s no clear, documented guarantee that Convex Auth’s signIn("github") flow is compatible with expectAuth in all setups. The behavior you’re seeing (OAuth not progressing, auth:signIn never called) is consistent with expectAuth blocking the very calls that would establish the session, but the sources don’t show a concrete fix for this specific combination.

  2. Is signIn() itself blocked by expectAuth?

    The knowledge sources don’t show the internal implementation of Convex Auth’s signIn, so I can’t say definitively. What we do see:

#
  • With Better Auth and WorkOS, when expectAuth is on, auth-related flows can hang or never complete until expectAuth is disabled. [Infinite loading thread; Better Auth + Convex thread; WorkOS thread 1]

    That strongly suggests that if signIn("github") depends on Convex client calls that are subject to expectAuth, they can be blocked before the first token is established—exactly the deadlock you’re describing. But the sources don’t explicitly confirm this for Convex Auth’s OAuth.

  1. What’s the recommended workaround right now?

    Based on the docs and threads, the safest patterns today are:

#
  • Turn off expectAuth and instead:
    • Use <Authenticated>, <Unauthenticated>, and <AuthLoading> or useConvexAuth() to gate components that run authenticated queries/mutations. [Infinite loading thread; WorkOS thread 3]
    • This avoids the deadlock and is what support recommends in multiple threads.
    • If you really want expectAuth:
      • Only use it on routes that are fully members‑only and where you can ensure a token is already available (e.g., via SSR + passing token into the Convex client, as in the TanStack Start + Better Auth example). [Better Auth migration / TanStack Start]
      • Avoid using it on the same page where the OAuth redirect/handshake is initiated unless you’ve verified that the sign‑in flow doesn’t depend on any Convex client calls that are being held back.

Because the knowledge sources don’t show a confirmed, working recipe for “Convex Auth + OAuth + expectAuth with no flashes,” I can’t give you a guaranteed configuration that fixes your hanging issue. The current guidance from support is effectively:

  • Treat expectAuth as experimental and fragile.
  • Prefer auth‑gated components (<Authenticated> / useConvexAuth) over expectAuth for now.
  • Use expectAuth only in carefully controlled, fully‑authed pages where the token is already known before any Convex client work happens.
#

If you want to push further on this specific bug (OAuth not progressing when expectAuth is on), the next step would be to share a minimal repro in Discord; similar race-condition issues are being tracked and iterated on there.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@honest trellis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

honest trellis
#

For reference:

  • The app is built with Vite + TanStack Router + TanStack Query + Convex + Convex Auth
  • No TanStack Start, so no SSR - full SPA.
  • It is in fact an app that adopts the "Members-only" pattern:
// In __root.tsx
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
  component: RootComponent,
});

function RootComponent() {
  return (
    <>
      <Unauthenticated>
        <SignInComponent />
      </Unauthenticated>

      <Authenticated>
        <SidebarProvider>
          <GlobalSidebar />
          <SidebarInset className="bg-transparent">
            <GlobalHeader />
            <Outlet />
          </SidebarInset>
        </SidebarProvider>
      </Authenticated>

      <AuthLoading>
        <SplashScreen />
      </AuthLoading>

      <Toaster richColors />

      <TanStackDevtools
        config={{
          position: "bottom-right",
        }}
        plugins={[
          {
            name: "TanStack Router",
            render: <TanStackRouterDevtoolsPanel />,
          },
          {
            name: "TanStack Query",
            render: <ReactQueryDevtools />,
          },
        ]}
      />
    </>
  );
}

@inner fox

inner foxBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re in a good pattern (SPA, members‑only, Convex Auth, TanStack Query/Router), but the knowledge sources don’t describe this exact combo with Convex Auth + expectAuth, so I can’t point to an official “this is the supported setup.” I can only relate what’s documented for similar stacks and what’s known about expectAuth.

What expectAuth is doing

expectAuth tells the Convex client to hold back all queries, mutations, and actions until the first auth token can be sent. [Convex client options; Base client options]

This works best when:

  • The page is truly members‑only, and
  • The auth token is available very early (often via SSR or a preloaded token). [Better Auth TanStack Start]

In other integrations (Better Auth, WorkOS, Clerk), support has seen:

  • Infinite loading / “auth never resolves” when expectAuth is on and the auth flow can’t complete because the Convex client is blocked. [Better Auth + Convex thread]
  • Recommendation to turn off expectAuth to debug auth, then only turn it back on once you’re sure the token is wired correctly. [Better Auth + Convex thread; Roast thread]

Your symptom:

  • With expectAuth: true: signIn("github") hangs, auth:signIn never fires.
  • With expectAuth: false: OAuth works, but you see UI flashes.
#

That’s consistent with the general pattern: expectAuth is blocking the Convex client before the auth flow can complete, so Convex Auth never gets to finish its sign‑in work. The sources don’t show Convex Auth’s internal implementation, so I can’t confirm exactly which call is blocked, only that this class of deadlock is known with other providers.

How your layout fits into this

Your root layout:

<Unauthenticated>
  <SignInComponent />
</Unauthenticated>

<Authenticated>
  {/* app shell */}
</Authenticated>

<AuthLoading>
  <SplashScreen />
</AuthLoading>

This is exactly the pattern Convex recommends for gating UI based on Convex’s own auth state. [AuthKit React example] It’s also the pattern support recommends instead of relying on expectAuth alone in other integrations. [WorkOS thread; Roast thread]

Given that:

  • You’re already using <Authenticated>/<Unauthenticated>/<AuthLoading>.
  • You’re SPA‑only (no SSR, no preloaded token).
  • expectAuth is experimental and known to cause hangs when auth can’t complete. [Convex client options; Better Auth + Convex thread]

The safest, documented path right now is:

  1. Leave expectAuth off for this SPA setup.
  2. Rely on <AuthLoading> to cover the “flash” while Convex is establishing auth:
    • Show your SplashScreen there (as you already do).
    • Only render any Convex queries/mutations inside <Authenticated>.
#

This is exactly how Convex suggests avoiding race conditions in other stacks: don’t render authed components until Convex’s auth is ready, instead of trying to globally block the client with expectAuth. [AuthKit React example; Redirect race thread]

About the “flash”

The sources don’t give a recipe for “no visual flash at all” in a SPA with no SSR. They do show:

So in your case, the “no flash” story is:

  • Make SplashScreen (inside <AuthLoading>) visually match either your sign‑in screen or your app shell as much as possible.
  • Keep all Convex‑using components strictly under <Authenticated> so nothing authed renders before Convex is ready.
#

There’s no documented way today to keep expectAuth on in your SPA and still have Convex Auth’s OAuth flow work correctly; the hanging behavior you see is in line with other reported expectAuth issues, and the guidance from support is to avoid expectAuth unless you have a pre‑established token (typically via SSR). [Better Auth + Convex thread; Roast thread]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@honest trellis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

honest trellis
#

Ran AI analysis, is it accurate? @inner fox

"1. expectAuth behavior

From node_modules/convex/dist/cjs-types/browser/sync/client.d.ts:

/**
 *
 * Whether query, mutation, and action requests should be held back
 * until the first auth token can be sent.
 *
 */
expectAuth?: boolean;

So with expectAuth: true, the client holds back query, mutation, and action requests until it has an auth token.

  1. signIn uses authenticatedCall

From node_modules/@convex-dev/auth/dist/react/client.js:

const signIn = useCallback(async (provider, args) => {
    const params = args instanceof FormData
        ? Array.from(args.entries()).reduce((acc, [key, value]) => {
            acc[key] = value;
            return acc;
        }, {})
        : args ?? {};
    const verifier = (await storageGet(VERIFIER_STORAGE_KEY)) ?? undefined;
    await storageRemove(VERIFIER_STORAGE_KEY);
    const result = await client.authenticatedCall("auth:signIn", { provider, params, verifier });

So signIn calls client.authenticatedCall("auth:signIn", ...).

  1. authenticatedCall uses the main client

From node_modules/@convex-dev/auth/dist/react/index.js:

const authClient = useMemo(() => ({
    authenticatedCall(action, args) {
        return client.action(action, args);
    },
    unauthenticatedCall(action, args) {
        return new ConvexHttpClient(client.address, {
            logger: client.logger,
        }).action(action, args);
    },

So:
authenticatedCall → client.action() (the main ConvexReactClient)
unauthenticatedCall → ConvexHttpClient.action() (separate HTTP client)

  1. Putting it together

With expectAuth: true, the main client does not send actions until it has an auth token.
signIn uses authenticatedCall, which goes through that main client.
Before sign-in, there is no token, so the auth:signIn action is never sent and the flow hangs."

inner foxBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Your analysis matches what we can infer from the behavior described in the knowledge sources, but the specific code paths you quoted (from node_modules) are not present in the provided docs/threads, so I can’t independently verify those exact snippets.

Here’s what is confirmed by the sources and how it lines up with your reasoning:

  1. expectAuth behavior

    The docs explicitly say:

    Whether query, mutation, and action requests should be held back until the first auth token can be sent. [Convex React client options; Base client options]

    So your point (1) is accurate: with expectAuth: true, the main Convex client will not send queries/mutations/actions until it has an auth token.

  2. Auth flows hanging with expectAuth

    Multiple threads report exactly the kind of behavior you’re seeing:

    That strongly supports your conclusion that expectAuth can deadlock an auth flow if that flow depends on actions going through the main client before any token exists.

#
  1. signIn using an authenticated path

    The sources don’t show the signIn implementation for Convex Auth, but they do show the general pattern for Better Auth and other integrations:

    • There’s a distinction between “authenticated” calls that go through the main Convex client and “unauthenticated” calls that go through an HTTP client. [Better Auth TanStack Start]
    • When expectAuth is enabled, anything that goes through the main client is subject to the “hold back until token” rule. [Convex React client options]

    Your reconstruction:

    • signInauthenticatedCall("auth:signIn", …)client.action("auth:signIn", …) (main client)
    • expectAuth: true → main client won’t send that action until it has a token
    • But that action is what would create the session / token

    is logically consistent with the behavior reported in the threads, even though the exact authenticatedCall implementation you pasted isn’t in the provided sources.

  2. Conclusion on your analysis

    Based on the docs and Discord threads:

#

The only part I can’t directly confirm from the knowledge sources is the exact internal code you quoted from node_modules, but the high‑level reasoning you derived from it matches the documented semantics and observed behavior. So as a model of “why signIn("github") hangs when expectAuth is true,” your explanation is accurate given the available information.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@honest trellis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

honest trellis
#

So basically chalk it up as #justexperimentalthings and disable it for now? @inner fox

inner foxBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes—that’s the safest move for your setup right now.

The docs explicitly mark expectAuth as experimental and say it holds back all queries, mutations, and actions until the first auth token can be sent. [Convex client options] In multiple Discord threads, when people see hangs or infinite loading with auth, support’s first recommendation is to turn off expectAuth to get auth working reliably, and only consider it later in very controlled cases (often with SSR and a pre‑known token). [Better Auth + Convex; Infinite loading]

Given:

  • You’re SPA‑only (no SSR / preloaded token),
  • Your OAuth flow clearly breaks when expectAuth is on,
  • You already gate UI with <Authenticated> / <Unauthenticated> / <AuthLoading>,

the documented, low‑risk path is:

  • Leave expectAuth disabled,
  • Keep using the Convex auth components/hooks to avoid race conditions.

If/when Convex documents a stable pattern for Convex Auth + expectAuth in SPAs, you could revisit it, but the current sources don’t show such a recipe.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: