Documentation Index
Fetch the complete documentation index at: https://mintlify.com/get-convex/better-auth/llms.txt
Use this file to discover all available pages before exploring further.
These features are experimental and may be changed or removed in the future.
JWT caching
Faster page loads and navigation by reusing JWT from cookies.
Authenticated queries in SSR require an additional request for a token. This can slow down initial page load and navigation for frameworks that server render on in-app navigation.
JWT caching allows server utilities like fetchAuthQuery to reuse the Convex JWT from request cookies if it is present and unexpired.
First, create a utility function for determining whether an error is auth related.
import { ConvexError } from "convex/values";
export const isAuthError = (error: unknown) => {
// This broadly matches potentially auth related errors, can be rewritten to
// work with your app's own error handling.
const message =
(error instanceof ConvexError && error.data) ||
(error instanceof Error && error.message) ||
"";
return /auth/i.test(message);
};
Configure jwtCache in your server utilities.
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";
import { isAuthError } from "@/lib/utils";
export const { fetchAuthQuery } = convexBetterAuthNextJs({
convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
jwtCache: {
enabled: true,
isAuthError,
},
});
import { convexBetterAuthReactStart } from "@convex-dev/better-auth/react-start";
import { isAuthError } from "@/lib/utils";
export const { fetchAuthQuery } = convexBetterAuthReactStart({
convexUrl: process.env.VITE_CONVEX_URL!,
convexSiteUrl: process.env.VITE_CONVEX_SITE_URL!,
jwtCache: {
enabled: true,
isAuthError,
},
});
Static JWKS
Faster page loads, navigation, and client readiness by speeding up Convex backend token validation.
When a Convex function is called over HTTP (as opposed to the WebSocket client), every request must include a token that the Convex backend validates. Token validation is never cached. By default, validation requires two blocking HTTP requests in serial: one for OIDC discovery to get the JWKS URL, and one to fetch the JWKS. The JWT is then validated against the JWKS.
By default the Better Auth component uses Convex’s customJwt to avoid the OIDC request by providing the JWKS URL statically in the auth config. Static JWKS goes further, avoiding both calls by providing the JWKS itself as a data URI.
Step 1. Add an internal action to fetch the latest JWKS. This must be an action (not a query) because a new key may be written.
export const getLatestJwks = internalAction({
args: {},
handler: async (ctx) => {
const auth = createAuth(ctx);
// This method is added by the Convex Better Auth plugin and is
// available via `auth.api` only, not exposed as a route.
return await auth.api.getLatestJwks();
},
});
Step 2. Run the action and set the JWKS environment variable from the CLI.
npx convex run auth:getLatestJwks | npx convex env set JWKS
Step 3. Provide the JWKS environment variable to your auth config and the Convex Better Auth plugin.
export default {
providers: [getAuthConfigProvider({ jwks: process.env.JWKS })],
} satisfies AuthConfig;
export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
return {
baseURL: siteUrl,
database: authComponent.adapter(ctx),
// ... other auth config
plugins: [
// ... other plugins
convex({
authConfig,
jwks: process.env.JWKS,
}),
],
} satisfies BetterAuthOptions;
};
AuthBoundary
The AuthBoundary React component wraps authenticated parts of your app and handles auth-related errors. It subscribes to a session-validated user query so that session state changes are handled consistently across the client.
Step 1. Export getAuthUser from the component’s client API.
export const { getAuthUser } = authComponent.clientApi();
Step 2. Create an isAuthError utility function.
import { ConvexError } from "convex/values";
export const isAuthError = (error: unknown) => {
// This broadly matches potentially auth related errors, can be rewritten to
// work with your app's own error handling.
const message =
(error instanceof ConvexError && error.data) ||
(error instanceof Error && error.message) ||
"";
return /auth/i.test(message);
};
Step 3. Create a ClientAuthBoundary component to configure and wrap AuthBoundary.
"use client";
import { useRouter } from "next/navigation";
import { AuthBoundary } from "@convex-dev/better-auth/react";
import { api } from "@/convex/_generated/api";
import { isAuthError } from "@/lib/utils";
import { authClient } from "@/lib/auth-client";
export const ClientAuthBoundary = ({ children }: PropsWithChildren) => {
const router = useRouter();
return (
<AuthBoundary
authClient={authClient}
// This can do anything you like, a redirect is typical.
onUnauth={() => router.replace("/sign-in")}
getAuthUserFn={api.auth.getAuthUser}
isAuthError={isAuthError}
>
{children}
</AuthBoundary>
);
};
"use client";
import { useNavigate } from "@tanstack/react-router";
import { AuthBoundary } from "@convex-dev/better-auth/react";
import { api } from "@/convex/_generated/api";
import { isAuthError } from "@/lib/utils";
import { authClient } from "@/lib/auth-client";
export const ClientAuthBoundary = ({ children }: PropsWithChildren) => {
const navigate = useNavigate();
return (
<AuthBoundary
authClient={authClient}
// This can do anything you like, a redirect is typical.
onUnauth={() => navigate({ to: "/sign-in" })}
getAuthUserFn={api.auth.getAuthUser}
isAuthError={isAuthError}
>
{children}
</AuthBoundary>
);
};
Step 4. Wrap your authenticated layout or route with ClientAuthBoundary. Most apps only need one.
import { isAuthenticated } from "@/lib/auth-server";
import { ClientAuthBoundary } from "@/lib/auth-client";
import { redirect } from "next/navigation";
import { PropsWithChildren } from "react";
export default async function Layout({ children }: PropsWithChildren) {
if (!(await isAuthenticated())) {
redirect("/sign-in");
}
return <ClientAuthBoundary>{children}</ClientAuthBoundary>;
}
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { ClientAuthBoundary } from '@/lib/auth-client'
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (!context.isAuthenticated) {
throw redirect({ to: "/sign-in" });
}
},
component: () => {
return (
<ClientAuthBoundary>
<Outlet />
</ClientAuthBoundary>
);
},
});