Basic Usage
For most things, you can follow the Better Auth documentation for basic usage. The Convex component is designed to provide a compatibility layer, so things generally work as expected. However, there are some things that do work differently with this component, those are documented here.
Signing in
Section titled “Signing in”Below is an extremely basic example of a working auth flow with email (unverified) and password.
3 collapsed lines
import { useState } from "react";import { authClient } from "@/lib/auth-client";import { api } from "../convex/_generated/api";import { Authenticated, Unauthenticated, AuthLoading, useQuery,} from "convex/react";
export default function App() { return ( <> <AuthLoading> <div>Loading...</div> </AuthLoading> <Unauthenticated> <SignIn /> </Unauthenticated> <Authenticated> <Dashboard /> </Authenticated> </> );}
function Dashboard() { const user = useQuery(api.auth.getCurrentUser); return ( <div> <div>Hello {user?.name}!</div> <button onClick={() => authClient.signOut()}>Sign out</button> </div> );}
function SignIn() { const [showSignIn, setShowSignIn] = useState(true);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); if (showSignIn) { await authClient.signIn.email( { email: formData.get("email") as string, password: formData.get("password") as string, }, { onError: (ctx) => { window.alert(ctx.error.message); }, } ); } else { await authClient.signUp.email( { name: formData.get("name") as string, email: formData.get("email") as string, password: formData.get("password") as string, }, { onError: (ctx) => { window.alert(ctx.error.message); }, } ); } };
17 collapsed lines
return ( <> <form onSubmit={handleSubmit}> {!showSignIn && <input name="name" placeholder="Name" />} <input type="email" name="email" placeholder="Email" /> <input type="password" name="password" placeholder="Password" /> <button type="submit">{showSignIn ? "Sign in" : "Sign up"}</button> </form> <p> {showSignIn ? "Don't have an account? " : "Already have an account? "} <button onClick={() => setShowSignIn(!showSignIn)}> {showSignIn ? "Sign up" : "Sign in"} </button> </p> </> );}
5 collapsed lines
"use client";
import { useState } from "react";import { authClient } from "@/lib/auth-client";import { api } from "../convex/_generated/api";import { Authenticated, Unauthenticated, AuthLoading, useQuery,} from "convex/react";
export default function App() { return ( <> <AuthLoading> <div>Loading...</div> </AuthLoading> <Unauthenticated> <SignIn /> </Unauthenticated> <Authenticated> <Dashboard /> </Authenticated> </> );}
function Dashboard() { const user = useQuery(api.auth.getCurrentUser); return ( <div> <div>Hello {user?.name}!</div> <button onClick={() => authClient.signOut()}>Sign out</button> </div> );}
function SignIn() { const [showSignIn, setShowSignIn] = useState(true);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); if (showSignIn) { await authClient.signIn.email( { email: formData.get("email") as string, password: formData.get("password") as string, }, { onError: (ctx) => { window.alert(ctx.error.message); }, } ); } else { await authClient.signUp.email( { name: formData.get("name") as string, email: formData.get("email") as string, password: formData.get("password") as string, }, { onError: (ctx) => { window.alert(ctx.error.message); }, } ); } };
17 collapsed lines
return ( <> <form onSubmit={handleSubmit}> {!showSignIn && <input name="name" placeholder="Name" />} <input type="email" name="email" placeholder="Email" /> <input type="password" name="password" placeholder="Password" /> <button type="submit">{showSignIn ? "Sign in" : "Sign up"}</button> </form> <p> {showSignIn ? "Don't have an account? " : "Already have an account? "} <button onClick={() => setShowSignIn(!showSignIn)}> {showSignIn ? "Sign up" : "Sign in"} </button> </p> </> );}
3 collapsed lines
import { useState } from "react";import { authClient } from "~/lib/auth-client";import { api } from "../convex/_generated/api";import { Authenticated, Unauthenticated, AuthLoading, useQuery,} from "convex/react";
export default function App() { return ( <> <AuthLoading> <div>Loading...</div> </AuthLoading> <Unauthenticated> <SignIn /> </Unauthenticated> <Authenticated> <Dashboard /> </Authenticated> </> );}
function Dashboard() { const user = useQuery(api.auth.getCurrentUser); return ( <div> <div>Hello {user?.name}!</div> <button onClick={() => authClient.signOut()}>Sign out</button> </div> );}
function SignIn() { const [showSignIn, setShowSignIn] = useState(true);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); if (showSignIn) { await authClient.signIn.email( { email: formData.get("email") as string, password: formData.get("password") as string, }, { onError: (ctx) => { window.alert(ctx.error.message); }, } ); } else { await authClient.signUp.email( { name: formData.get("name") as string, email: formData.get("email") as string, password: formData.get("password") as string, }, { onError: (ctx) => { window.alert(ctx.error.message); }, } ); } };
17 collapsed lines
return ( <> <form onSubmit={handleSubmit}> {!showSignIn && <input name="name" placeholder="Name" />} <input type="email" name="email" placeholder="Email" /> <input type="password" name="password" placeholder="Password" /> <button type="submit">{showSignIn ? "Sign in" : "Sign up"}</button> </form> <p> {showSignIn ? "Don't have an account? " : "Already have an account? "} <button onClick={() => setShowSignIn(!showSignIn)}> {showSignIn ? "Sign up" : "Sign in"} </button> </p> </> );}
Server side
Section titled “Server side”Using auth.api
Section titled “Using auth.api”For full stack frameworks like Next.js and TanStack Start, Better Auth provides server side functionality via auth.api
methods. With Convex, you would instead run these methods in your Convex functions.
import { auth } from "./auth"; import { createAuth } from "./auth";
// Example: viewing backup codes with the Two Factor plugin
export const getBackupCodes = () => { return auth.api.viewBackupCodes({ body: { userId: "user-id" } }) } export const getBackupCodes = query({ args: { userId: v.id("users"), }, handler: async (ctx, args) => { const auth = createAuth(ctx); return await auth.api.viewBackupCodes({ body: { userId: args.userId, }, }); }, });
Sessions
Section titled “Sessions”Accessing the session server side requires request headers. The Convex component provides a method for generating headers for the current session.
import { createAuth, betterAuthComponent } from "./auth";
export const getSession = query({ args: {}, handler: async (ctx) => { const auth = createAuth(ctx); const headers = await betterAuthComponent.getHeaders(ctx); const session = await auth.api.getSession({ headers, }); if (!session) { return null; } // Do something with the session return session; },});
Server-side auth
Section titled “Server-side auth”Server-side authentication with the Better Auth component works similar to other Convex authentication providers. See the Convex docs for your framework for more details.
Server side authentication with Convex requires a token. To get an identity token with Better Auth, use the framework appropriate getToken
approach.
"use server";
import { api } from "@/convex/_generated/api";import { getToken } from "@convex-dev/better-auth/nextjs";import { createAuth } from "@/convex/auth";import { fetchMutation } from "convex/nextjs";
// Authenticated mutation via server functionexport async function createPost(title: string, content: string) { const token = await getToken(createAuth); await fetchMutation(api.posts.create, { title, content }, { token });}
import { createServerFn } from "@tanstack/react-start";import { getCookieName } from "@convex-dev/better-auth/react-start";import { createAuth } from "../../convex/auth";import { api } from "../../convex/_generated/api";import { ConvexHttpClient } from "convex/browser";
const setupClient = (token?: string) => { const client = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL) if (token) { client.setAuth(token) } return client}
const getToken = async () => { const sessionCookieName = await getCookieName(createAuth) return getCookie(sessionCookieName)}
export const createPost = createServerFn({ method: 'POST' }) .handler(async ({ data: { title, content } }) => { const token = await getToken() await setupClient(token).mutation(api.posts.create, { title, content, }) })
Authorization
Section titled “Authorization”To check authentication state in your React components, use the authentication state components from convex/react
.
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";
export default function App() { return ( <> <AuthLoading> <div>Loading...</div> </AuthLoading> <Authenticated> <div>Dashboard</div> </Authenticated> <Unauthenticated> <div>Sign In</div> </Unauthenticated> </> );}
Convex Functions
Section titled “Convex Functions”For authorization and user checks inside Convex functions (queries, mutations, actions), use Convex’s ctx.auth
or the getAuthUserId()
/getAuthUser()
methods on the Better Auth Convex component:
import { betterAuthComponent } from "./auth";import { Id } from "./_generated/dataModel";
export const myFunction = query({ args: {}, handler: async (ctx) => { // You can get the user id directly from Convex via ctx.auth const identity = await ctx.auth.getUserIdentity(); if (!identity) { return null; } // For now the id type requires an assertion const userIdFromCtx = identity.subject as Id<"users">;
// The component provides a convenience method to get the user id const userId = await betterAuthComponent.getAuthUserId(ctx); if (!userId) { return null; }
const user = await ctx.db.get(userId as Id<"users">);
// Get user email and other metadata from the Better Auth component const userMetadata = await betterAuthComponent.getAuthUser(ctx);
// You can combine them if you want return { ...userMetadata, ...user }; },});