Skip to main content

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.
lib/utils.ts
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.
lib/auth-server.ts
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,
  },
});

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.
convex/auth.ts
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.
convex/auth.config.ts
export default {
  providers: [getAuthConfigProvider({ jwks: process.env.JWKS })],
} satisfies AuthConfig;
convex/auth.ts
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.
convex/auth.ts
export const { getAuthUser } = authComponent.clientApi();
Step 2. Create an isAuthError utility function.
lib/utils.ts
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.
lib/auth-client.tsx
"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>
  );
};
Step 4. Wrap your authenticated layout or route with ClientAuthBoundary. Most apps only need one.
app/(auth)/layout.tsx
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>;
}