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.

A complete working example is available in the GitHub repo.
TanStack Start uses convexBetterAuthReactStart for server utilities and ConvexBetterAuthProvider for client-side token management. Auth requests are proxied through a TanStack route handler to your Convex deployment.

Installation

1

Install packages

Install the component, ensure you have the latest version of Convex, and install a pinned version of Better Auth.
This component requires Convex 1.25.0 or later. Install better-auth@1.5.3 with an exact pin to avoid unexpected breaking changes.
npm install convex@latest @convex-dev/better-auth
npm install better-auth@1.5.3 --save-exact
npm install @types/node --save-dev
2

Configure Vite for SSR

Configure Vite to bundle @convex-dev/better-auth during SSR to avoid module resolution issues.
vite.config.ts
export default defineConfig({
  // ...other config
  ssr: {
    noExternal: ['@convex-dev/better-auth'],
  },
});
3

Register the component

Register the Better Auth component in your Convex project configuration.
convex/convex.config.ts
import { defineApp } from "convex/server";
import betterAuth from "@convex-dev/better-auth/convex.config";

const app = defineApp();
app.use(betterAuth);

export default app;
4

Add Convex auth config

Add a convex/auth.config.ts file to configure Better Auth as a Convex authentication provider.
convex/auth.config.ts
import { getAuthConfigProvider } from '@convex-dev/better-auth/auth-config'
import type { AuthConfig } from 'convex/server'

export default {
  providers: [getAuthConfigProvider()],
} satisfies AuthConfig
5

Set environment variables

Generate a secret for encryption and hashing, and set your site URL on your Convex deployment.
npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
npx convex env set SITE_URL http://localhost:3000
Then add the client-side environment variables to the .env.local file created by npx convex dev.
.env.local
# Deployment used by `npx convex dev`
CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name

VITE_CONVEX_URL=https://adjective-animal-123.convex.cloud

# Same as VITE_CONVEX_URL but ends in .site
VITE_CONVEX_SITE_URL=https://adjective-animal-123.convex.site

# Your local site URL
VITE_SITE_URL=http://localhost:3000
6

Create a Better Auth server instance

Create the Better Auth instance in convex/auth.ts and initialize the component.
Some TypeScript errors will appear until you save the file and generated types are updated.
convex/auth.ts
import { betterAuth } from 'better-auth/minimal'
import { createClient } from '@convex-dev/better-auth'
import { convex } from '@convex-dev/better-auth/plugins'
import authConfig from './auth.config'
import { components } from './_generated/api'
import { query } from './_generated/server'
import type { GenericCtx } from '@convex-dev/better-auth'
import type { DataModel } from './_generated/dataModel'

const siteUrl = process.env.SITE_URL!

// The component client has methods needed for integrating Convex with Better Auth,
// as well as helper methods for general use.
export const authComponent = createClient<DataModel>(components.betterAuth)

export const createAuth = (ctx: GenericCtx<DataModel>) => {
  return betterAuth({
    baseURL: siteUrl,
    database: authComponent.adapter(ctx),
    // Configure simple, non-verified email/password to get started
    emailAndPassword: {
      enabled: true,
      requireEmailVerification: false,
    },
    plugins: [
      // The Convex plugin is required for Convex compatibility
      convex({ authConfig }),
    ],
  })
}

// Example function for getting the current user
// Feel free to edit, omit, etc.
export const getCurrentUser = query({
  args: {},
  handler: async (ctx) => {
    return await authComponent.getAuthUser(ctx)
  },
})
7

Create a Better Auth client instance

Create a Better Auth client instance for your frontend.
src/lib/auth-client.ts
import { createAuthClient } from 'better-auth/react'
import { convexClient } from '@convex-dev/better-auth/client/plugins'

export const authClient = createAuthClient({
  plugins: [convexClient()],
})
8

Configure TanStack server utilities

Create server utilities for authenticated SSR and server functions using convexBetterAuthReactStart. This gives you handler, getToken, fetchAuthQuery, fetchAuthMutation, and fetchAuthAction.
src/lib/auth-server.ts
import { convexBetterAuthReactStart } from '@convex-dev/better-auth/react-start'

export const {
  handler,
  getToken,
  fetchAuthQuery,
  fetchAuthMutation,
  fetchAuthAction,
} = convexBetterAuthReactStart({
  convexUrl: process.env.VITE_CONVEX_URL!,
  convexSiteUrl: process.env.VITE_CONVEX_SITE_URL!,
})
9

Mount HTTP route handlers

Register the Better Auth route handlers on your Convex HTTP router.
convex/http.ts
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";

const http = httpRouter();

authComponent.registerRoutes(http, createAuth);

export default http;
Then add a TanStack catch-all route to proxy auth requests to your Convex deployment.
src/routes/api/auth/$.ts
import { createFileRoute } from '@tanstack/react-router'
import { handler } from '~/lib/auth-server'

export const Route = createFileRoute('/api/auth/$')({
  server: {
    handlers: {
      GET: ({ request }) => handler(request),
      POST: ({ request }) => handler(request),
    },
  },
})
10

Set up the root route

Wrap your application root with ConvexBetterAuthProvider and make auth available in loaders via beforeLoad.
src/routes/__root.tsx
/// <reference types="vite/client" />
import {
  HeadContent,
  Outlet,
  Scripts,
  createRootRouteWithContext,
  useRouteContext,
} from '@tanstack/react-router'
import * as React from 'react'
import { createServerFn } from '@tanstack/react-start'
import { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'
import type { ConvexQueryClient } from '@convex-dev/react-query'
import type { QueryClient } from '@tanstack/react-query'
import appCss from '~/styles/app.css?url'
import { authClient } from '~/lib/auth-client'
import { getToken } from '~/lib/auth-server'

// Get auth information for SSR using available cookies
const getAuth = createServerFn({ method: 'GET' }).handler(async () => {
  return await getToken()
})

export const Route = createRootRouteWithContext<{
  queryClient: QueryClient
  convexQueryClient: ConvexQueryClient
}>()({
  head: () => ({
    meta: [
      { charSet: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    ],
    links: [
      { rel: 'stylesheet', href: appCss },
      { rel: 'icon', href: '/favicon.ico' },
    ],
  }),
  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,
})

function RootComponent() {
  const context = useRouteContext({ from: Route.id })
  return (
    <ConvexBetterAuthProvider
      client={context.convexQueryClient.convexClient}
      authClient={authClient}
      initialToken={context.token}
    >
      <RootDocument>
        <Outlet />
      </RootDocument>
    </ConvexBetterAuthProvider>
  )
}

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="dark">
      <head>
        <HeadContent />
      </head>
      <body className="bg-neutral-950 text-neutral-50">
        {children}
        <Scripts />
      </body>
    </html>
  )
}
11

Add route context

Provide the Convex query client context to your routes and configure SSR query integration. This may replace some existing router setup in your code.
src/routes/router.tsx
import { createRouter } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
// You may need to install this package if you haven't already
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { ConvexQueryClient } from '@convex-dev/react-query'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  if (typeof document !== 'undefined') {
    notifyManager.setScheduler(window.requestAnimationFrame)
  }

  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 = createRouter({
    routeTree,
    defaultPreload: 'intent',
    context: { queryClient, convexQueryClient },
    scrollRestoration: true,
    defaultErrorComponent: (err) => <p>{err.error.stack}</p>,
    defaultNotFoundComponent: () => <p>not found</p>,
  })

  setupRouterSsrQueryIntegration({
    router,
    queryClient,
  })

  return router
}
You’re now ready to use Better Auth with Convex in your TanStack Start app.

Usage

See Basic Usage for sign-in, sign-up, and session management patterns. The notes below are specific to TanStack Start.

SSR with TanStack Query

Use TanStack Query’s ensureQueryData and useSuspenseQuery to use Convex queries during server-side rendering.
A seamless initial render currently requires expectAuth: true in the ConvexQueryClient constructor. This setting prevents Convex functions from running in the client before authentication.
src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { api } from "~/convex/_generated/api";
import { convexQuery } from "@convex-dev/react-query";
import { useSuspenseQuery } from "@tanstack/react-query";

export const Route = createFileRoute("/")({
  component: App,
  loader: async ({ context }) => {
    await Promise.all([
      context.queryClient.ensureQueryData(
        convexQuery(api.auth.getCurrentUser, {})
      ),
      // Load multiple queries in parallel if needed
    ]);
  },
});

Signing out with expectAuth: true

The expectAuth: true setting only affects behavior before the initial authentication. If a user signs out and signs back in, authenticated queries may run before authentication is ready, causing an error. The current recommendation is to reload the page on sign out. For apps that redirect based on authentication, signing out is typically all that’s needed since an unauthenticated redirect will occur after reload.
src/routes/index.tsx
import { authClient } from "~/lib/auth-client";

const handleSignOut = async () => {
  await authClient.signOut({
    fetchOptions: {
      onSuccess: () => {
        location.reload();
      },
    },
  });
};

Using Better Auth from the server

Better Auth’s auth.api methods normally run in your server code, but with Convex as your backend, these methods run inside Convex functions. Call those functions from server functions using fetchAuthMutation or fetchAuthQuery. Authentication is handled automatically using session cookies. Here’s an example using changePassword. The Better Auth auth.api method runs inside a Convex mutation:
convex/users.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { createAuth, authComponent } from "./auth";

export const updateUserPassword = mutation({
  args: {
    currentPassword: v.string(),
    newPassword: v.string(),
  },
  handler: async (ctx, args) => {
    const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
    await auth.api.changePassword({
      body: {
        currentPassword: args.currentPassword,
        newPassword: args.newPassword,
      },
      headers,
    });
  },
});
Calling that mutation from a TanStack Start server function:
src/routes/users.ts
import { createServerFn } from "@tanstack/react-start";
import { fetchAuthMutation } from "@/lib/auth-server";
import { api } from "../../convex/_generated/api";

export const updatePassword = createServerFn({ method: "POST" }).handler(
  async ({ data: { currentPassword, newPassword } }) => {
    await fetchAuthMutation(api.users.updatePassword, {
      currentPassword,
      newPassword,
    });
  }
);