Migrate to 0.10
Migrate to @convex-dev/better-auth@0.10
Upgrade component
Install dependencies
Update Better Auth and the component.
Be sure to review breaking changes in Better Auth 1.4.
Check your dependencies for Better Auth packages and be sure to pin them all to 1.4.7. For example if you're using @better-auth/expo, it would also need to be upgraded.
npm install @convex-dev/better-auth@^0.10.0
npm install better-auth@1.4.7 --save-exactUpdate auth config
Use customJwt auth config
customJwt auth config is
now recommended for faster JWT validation. A helper is provided to generate the
config object.
import type { AuthConfig } from "convex/server";
import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
export default {
providers: [
getAuthConfigProvider(),
{
applicationID: "convex",
domain: process.env.CONVEX_SITE_URL!,
},
],
} satisfies AuthConfig;Update Convex Better Auth plugin config
The Convex Better Auth plugin now requires the auth config object. This helps minimize configuration and ensure accuracy.
Additionally, as this new configuration uses RS256 instead of EdDSA (for
compatibility with customJwt), you'll want to set
jwksRotateOnTokenGenerationError to true initially so keys can be rotated
when algorithm mismatch occurs. This can be disabled later if desired.
import authConfig from "./auth.config";
export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
return {
baseURL: siteUrl,
database: authComponent.adapter(ctx),
// ... other auth config
plugins: [
// ... other plugins
convex(),
convex({
authConfig,
jwksRotateOnTokenGenerationError: true,
}),
],
} satisfies BetterAuthOptions;
};Drop optionsOnly parameter from createAuth
The optionsOnly parameter is no longer needed.
export const createAuth = (
ctx: GenericCtx<DataModel>,
{ optionsOnly } = { optionsOnly: false },
) => {
export const createAuth = (ctx: GenericCtx<DataModel>) => {
return betterAuth({
logger: {
disabled: optionsOnly,
},
// ... auth config
});
}SSR improvements
The following steps are only necessary for apps using SSR (eg., Next.js, TanStack Start).
Update framework server utilities
Server utilities are provided for TanStack Start and Next.js to provide the same kind of set-it-and-forget-it auth management that you get in the client, on the server.
The configuration requires explicit URLs for Convex with runtime validation, which should reduce environment variable confusion.
Replace usage of previous versions of these functions with the new ones, eg.,
preloadQuery with preloadAuthQuery, fetchQuery with fetchAuthQuery, etc.
Note that the new functions only accept a function reference and arguments, a
third option with configuration such as a token is not accepted or required.
import { createAuth } from "@/convex/auth";
import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs";
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";
export const getToken = () => {
return getTokenNextjs(createAuth);
};
export const {
handler,
preloadAuthQuery,
isAuthenticated,
getToken,
fetchAuthQuery,
fetchAuthMutation,
fetchAuthAction,
} = convexBetterAuthNextJs({
convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});import { createAuth } from "convex/auth";
import { setupFetchClient } from "@convex-dev/better-auth/react-start";
import { getCookie } from "@tanstack/react-start/server";
import { convexBetterAuthReactStart } from "@convex-dev/better-auth/react-start";
export const { fetchQuery, fetchMutation, fetchAction } =
await setupFetchClient(createAuth, getCookie);
export const {
handler,
getToken,
fetchAuthQuery,
fetchAuthMutation,
fetchAuthAction,
} = convexBetterAuthReactStart({
convexUrl: process.env.VITE_CONVEX_URL!,
convexSiteUrl: process.env.VITE_CONVEX_SITE_URL!,
});Update route handlers
Update your route handlers to use the new handler.
import { nextJsHandler } from "@convex-dev/better-auth/nextjs";
import { handler } from "@/lib/auth-server";
export const { GET, POST } = nextJsHandler();
export const { GET, POST } = handler; import { createFileRoute } from '@tanstack/react-router'
import { reactStartHandler } from '@convex-dev/better-auth/react-start'
import { handler } from '~/lib/auth-server'
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: ({ request }) => reactStartHandler(request),
POST: ({ request }) => reactStartHandler(request),
GET: ({ request }) => handler(request),
POST: ({ request }) => handler(request),
},
},
})Pass initial token to ConvexBetterAuthProvider
Apps using Next.js or TanStack Start can pass the an initial token to speed up client authentication.
Update your ConvexClientProvider to accept an initial token.
export function ConvexClientProvider({
children,
initialToken,
}: {
children: ReactNode;
initialToken?: string | null;
}) {
return (
<ConvexBetterAuthProvider
client={convex}
authClient={authClient}
initialToken={initialToken}
>
{children}
</ConvexBetterAuthProvider>
);
}Then fetch the initial token on the server from your root layout and pass it to
the ConvexClientProvider.
import { getToken } from "@/lib/auth-server";
import { ConvexClientProvider } from "./ConvexClientProvider";
import { PropsWithChildren } from "react";
export default async function RootLayout({ children }: PropsWithChildren) {
const token = await getToken();
return (
<html>
<body>
<ConvexClientProvider>
<ConvexClientProvider initialToken={token}>
{children}
</ConvexClientProvider>
</body>
</html>
);
}function RootComponent() {
const context = useRouteContext({ from: Route.id })
return (
<ConvexBetterAuthProvider
client={context.convexQueryClient.convexClient}
authClient={authClient}
initialToken={context.token}
>
<RootDocument>
<Outlet />
</RootDocument>
</ConvexBetterAuthProvider>
)
}Next.js changes
Use usePreloadedAuthQuery
usePreloadedAuthQuery replaces usePreloadedQuery as a drop-in - it ensures
server rendered data is rendered until authentication is ready.
import { usePreloadedAuthQuery } from "@convex-dev/better-auth/nextjs/client";
export const TodoList = ({ preloadedUserQuery }) => {
const userQuery = usePreloadedAuthQuery(preloadedUserQuery);
return (
<div>
<h1>{userQuery?.name}</h1>
</div>
);
};TanStack Start changes
Update Vite configuration
Update Vite configuration to bundle @convex-dev/better-auth during SSR to
avoid module resolution issues.
export default defineConfig({
// ...other config
ssr: {
noExternal: ["@convex-dev/better-auth"],
},
});Update router config
Update the router to set expectAuth: true on the ConvexQueryClient - without
this, authenticated data from server render is lost immediately after first
render.
Also drop the unused Wrap provider (if you have one).
export function getRouter() {
const convexUrl = (import.meta as any).env.VITE_CONVEX_URL!;
if (!convexUrl) {
throw new Error("VITE_CONVEX_URL is not set");
}
const convexQueryClient = new ConvexQueryClient(convexUrl, {
expectAuth: true,
});
const queryClient: QueryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);
const router = createTanStackRouter({
routeTree,
defaultPreload: "intent",
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
Wrap: ({ children }) => (
<ConvexProvider client={convexQueryClient.convexClient}>
{children}
</ConvexProvider>
),
context: { queryClient, convexQueryClient },
scrollRestoration: true,
});
setupRouterSsrQueryIntegration({
router,
queryClient,
});
return router;
}Update root route
Update the root route to use the new getAuth() server function and
isAuthenticated context value.
import { createServerFn } from "@tanstack/react-start";
import {
fetchSession,
getCookieName,
} from "@convex-dev/better-auth/react-start";
import { getCookie, getRequest } from "@tanstack/react-start/server";
import { getToken } from "@/lib/auth-server";
const fetchAuth = createServerFn({ method: "GET" }).handler(async () => {
const { createAuth } = await import("@convex/auth");
const { session } = await fetchSession(getRequest());
const sessionCookieName = getCookieName(createAuth);
const token = getCookie(sessionCookieName);
return {
userId: session?.user.id,
token,
};
});
const getAuth = createServerFn({ method: "GET" }).handler(async () => {
return await getToken();
});
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
convexQueryClient: ConvexQueryClient;
}>()({
// ... other config
beforeLoad: async (ctx) => {
const token = await getAuth();
// all queries, mutations and actions through TanStack Query will be
// authenticated during SSR if we have a valid token
if (token) {
// During SSR only (the only time serverHttpClient exists),
// set the auth token to make HTTP queries with.
ctx.context.convexQueryClient.serverHttpClient?.setAuth(token);
}
return {
isAuthenticated: !!token,
token,
};
},
component: RootComponent,
});Reload on sign out
Finally, update your signOut function(s) to reload the page. This allows
expectAuth to work correctly for the next sign in. For apps with authenticated
routes, signing out can typically replace a redirect.
export const handleSignOut = async () => {
const navigate = useNavigate();
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate({ to: "/" });
location.reload();
},
},
});
};Local Install additional steps
The following steps are only necessary for apps using Local Install.
Remove getStaticAuth
Remove getStaticAuth in convex/betterAuth/auth.ts and replace it with a
simple createAuth
import { getStaticAuth } from "@convex-dev/better-auth";
import { v } from "convex/values";
import { createAuth } from "../auth";
// Export a static instance for Better Auth schema generation
export const auth = getStaticAuth(createAuth);
export const auth = createAuth({} as any); Regenerate schema
Whenever updating Better Auth w/ Local Install, regenerate the schema.
cd convex/betterAuth
npx @better-auth/cli generate -ySplit out createAuthOptions function
The previous approach of configuring Better Auth options inside of the
betterAuth() function call meant we had to run Better Auth to access options
statically. This led to unnecessary logging and inaccurate error messages, and
required workarounds like using optionsOnly to disable logging.
To avoid all of this moving forward, you'll want to have a separate
createAuthOptions function that just returns the typed options object.
import {
betterAuth,
type BetterAuthOptions,
} from "better-auth";
export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
return {
// ... auth config
} satisfies BetterAuthOptions;
};
export const createAuth = (ctx: GenericCtx<DataModel>) => {
return betterAuth({
// ... auth config
});
return betterAuth(createAuthOptions(ctx));
};Update adapter
Replace usage of createAuth with createAuthOptions in adapter.ts.
You'll also drop the removed migrationRemoveUserId function.
import { createApi } from "@convex-dev/better-auth";
import {
createAuth,
createAuthOptions,
} from "./auth";
import schema from "./schema";
export const {
create,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
migrationRemoveUserId,
} = createApi(schema, createAuth);
} = createApi(schema, createAuthOptions);