Skip to content
v3.0.1 — MCP OAuth, organizations, passkeys, SSR adaptersChangelog
Convex Auth
Esc
↑↓navigate↵open⌘Jpreview
On this page

TanStack Router

Client-side route guards with convex-auth in a TanStack Router app.

TanStack Router is a client-side router, so convex-auth protects routes the way it does in any React SPA: the session lives in React state, the router mirrors it into route context, and a pathless _authed layout guards protected routes in beforeLoad. This guide covers that pattern end to end — for SSR with server functions and cookie sessions, use TanStack Start instead.

Mirror the session into router context

useSession() can’t be called inside beforeLoad — guards run outside React. Instead, read the session once at the app root and pass it to RouterProvider as context:

// src/main.tsx
import { useEffect } from "react";
import { RouterProvider } from "@tanstack/react-router";
import { ConvexReactClient, ConvexProvider } from "convex/react";
import { ConvexAuthClientProvider, useSession } from "@vortex-api/convex-auth/react";
import { api } from "../convex/_generated/api";
import { router } from "./router";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

function InnerApp() {
  const { isLoading, isAuthenticated } = useSession();

  useEffect(() => {
    void router.invalidate();
  }, [isLoading, isAuthenticated]);

  return <RouterProvider router={router} context={{ auth: { isLoading, isAuthenticated } }} />;
}

// InnerApp must render inside the providers — useSession() throws otherwise:
createRoot(document.getElementById("root")!).render(
  <ConvexProvider client={convex}>
    <ConvexAuthClientProvider actions={api.auth}>
      <InnerApp />
    </ConvexAuthClientProvider>
  </ConvexProvider>,
);

router.invalidate() re-runs beforeLoad whenever the session flips, so guards react to sign-in and sign-out without a page reload.

// src/router.ts
export const router = createRouter({
  routeTree,
  context: {
    auth: { isLoading: true, isAuthenticated: false },
  },
});

Guard protected routes with a pathless layout

Wrap every protected page in an _authed layout route. Its beforeLoad reads the mirrored context and throws a redirect carrying the attempted URL:

// src/routes/_authed.tsx
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/_authed")({
  beforeLoad: ({ context, location }) => {
    if (context.auth.isLoading) {
      return;
    }
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: "/sign-in",
        search: { redirect: location.href },
      });
    }
  },
  component: AuthedLayout,
});

function AuthedLayout() {
  const { auth } = Route.useRouteContext();

  if (auth.isLoading) {
    return <p>Checking session…</p>;
  }

  return <Outlet />;
}

Any file under routes/_authed/ is now protected. While isLoading is true the guard waits — never redirect an unresolved session, or every refresh bounces signed-in users through /sign-in.

Send signed-in users back where they came from

The sign-in route validates the redirect search param and forwards it after auth:

// src/routes/sign-in.tsx
export const Route = createFileRoute("/sign-in")({
  validateSearch: (search: Record<string, unknown>) => ({
    redirect: typeof search.redirect === "string" ? search.redirect : undefined,
  }),
  component: SignInPage,
});

function SignInPage() {
  const { isLoading, isAuthenticated } = useSession();
  const { redirect } = Route.useSearch();

  if (isAuthenticated) {
    return <Navigate to={redirect ?? "/dashboard"} />;
  }

  return (
    <ConvexAuthSignInPage
      auth={{ isLoaded: !isLoading, isSignedIn: isAuthenticated }}
      signUpUrl="/sign-in"
      forceRedirectUrl={redirect ?? "/dashboard"}
    />
  );
}

Example app

examples/tanstack-router is a runnable Vite + React app using exactly this pattern — context mirroring, the _authed guard, and redirect-on-sign-in:

cd examples/tanstack-router
pnpm install
pnpm dlx convex dev   # anonymous local backend
pnpm run dev

Next steps

Was this page helpful?