Experimental
Experimental features for Convex + Better Auth
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 utilties like fetchAuthQuery to utilize the Convex
JWT from request cookies if 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 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 and be validated by the Convex backend. Token validation is never cached. By default, validation requires the Convex backend to make two blocking http requests serially: one for OIDC discovery, which provides the JWKS url, and one for fetching the JWKS from the url. The JWT is then validated using 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 avoids both calls by providing the JWKS itself as a data uri.
First, add an internal mutation to get the latest JWKS. A new key may be written, so this must be a mutation.
export const getLatestJwks = internalMutation({
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();
},
});Run the mutation and set the JWKS environment variable from the CLI.
npx convex run auth:getLatestJwks | npx convex env set JWKSProvide 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 is a wrapper that provides error handling for
auth related errors in the client. It subscribes to a session validated user
query for synced handling of session state changes.
The Convex Component exports a query for use with AuthBoundary.
export const { getAuthUser } = authComponent.clientApi(); 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);
};Add a client component to wrap and configure 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>
);
};Wrap authenticated layout, route, etc. with ClientAuthBoundary. Most apps can
just use 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) {
console.log("redirecting to /sign-in");
throw redirect({ to: "/sign-in" });
}
},
component: () => {
return (
<ClientAuthBoundary>
<Outlet />
</ClientAuthBoundary>
);
},
});