Better Auth provides built-in support for:
- Email and password
- Social providers (Google, GitHub, Apple, Discord, and more)
You can also extend these with plugins such as username, magic link, passkey, and email OTP.
Email & password
Enable email and password authentication in your auth instance:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
});
Sign up
Call signUp.email on the client with the user’s details:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signUp.email({
email, // user email address
password, // minimum 8 characters by default
name, // display name
image, // avatar URL (optional)
callbackURL: "/dashboard", // redirect after email verification (optional)
}, {
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
// redirect to dashboard or show success message
},
onError: (ctx) => {
alert(ctx.error.message);
},
});
By default, users are automatically signed in after registration. To disable this:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
autoSignIn: false, // defaults to true
},
});
Sign in
Call signIn.email on the client:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signIn.email({
email,
password,
callbackURL: "/dashboard",
/**
* Keep the session alive after the browser is closed.
* @default true
*/
rememberMe: false,
}, {
// optional callbacks
});
Always call client methods from the client side. Do not call them from the server.
Server-side authentication
To authenticate a user from your server, use auth.api methods directly:
import { auth } from "./auth";
const response = await auth.api.signInEmail({
body: {
email,
password,
},
asResponse: true, // returns a Response object instead of data
});
If the server cannot return a Response object, you’ll need to manually parse and set cookies. For Next.js, Better Auth provides a plugin to handle this automatically.
Social sign-on
Better Auth supports Google, GitHub, Apple, Discord, and many more social providers. Configure the providers you need on your auth instance:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
});
Sign in with a social provider
Call signIn.social on the client:
import { authClient } from "@/lib/auth-client";
await authClient.signIn.social({
/**
* The social provider ID.
* @example "github", "google", "apple"
*/
provider: "github",
/**
* Redirect after the user authenticates with the provider.
* @default "/"
*/
callbackURL: "/dashboard",
/**
* Redirect if an error occurs during sign-in.
*/
errorCallbackURL: "/error",
/**
* Redirect for newly registered users.
*/
newUserCallbackURL: "/welcome",
/**
* Disable the automatic redirect to the provider.
* @default false
*/
disableRedirect: true,
});
You can also authenticate using an idToken or accessToken from the social provider instead of redirecting the user. See the social providers documentation for details.
Sign out
Call signOut on the client:
import { authClient } from "@/lib/auth-client";
await authClient.signOut();
Pass fetchOptions to redirect on success:
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login");
},
},
});
Session management
Once a user is signed in, you can access their session data from both the client and server.
Client side
useSession hook
Better Auth provides a useSession hook backed by nanostores. It keeps your UI in sync — any change to the session (such as signing out) is reflected immediately.
React
Vue
Svelte
Solid
Vanilla
import { authClient } from "@/lib/auth-client";
export function User() {
const {
data: session,
isPending, // loading state
error, // error object
refetch, // manually refetch
} = authClient.useSession();
return (
// render session data
);
}
<script setup lang="ts">
import { authClient } from "~/lib/auth-client";
const session = authClient.useSession();
</script>
<template>
<div>
<pre>{{ session.data }}</pre>
<button v-if="session.data" @click="authClient.signOut()">
Sign out
</button>
</div>
</template>
<script lang="ts">
import { authClient } from "$lib/auth-client";
const session = authClient.useSession();
</script>
<p>{$session.data?.user.email}</p>
import { authClient } from "~/lib/auth-client";
export default function Home() {
const session = authClient.useSession();
return (
<pre>{JSON.stringify(session(), null, 2)}</pre>
);
}
import { authClient } from "~/lib/auth-client";
authClient.useSession.subscribe((value) => {
// react to session changes
});
getSession
If you prefer not to use the hook, call getSession directly:
import { authClient } from "@/lib/auth-client";
const { data: session, error } = await authClient.getSession();
This works with client-side data-fetching libraries like TanStack Query.
Server side
Pass the incoming request headers to auth.api.getSession:
Next.js
Nuxt
SvelteKit
Astro
Hono
TanStack Start
import { auth } from "./auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({
headers: await headers(),
});
import { auth } from "~/utils/auth";
export default defineEventHandler(async (event) => {
const session = await auth.api.getSession({
headers: event.headers,
});
});
import { auth } from "./auth";
export async function load({ request }) {
const session = await auth.api.getSession({
headers: request.headers,
});
return { props: { session } };
}
---
import { auth } from "./auth";
const session = await auth.api.getSession({
headers: Astro.request.headers,
});
---
import { auth } from "./auth";
const app = new Hono();
app.get("/path", async (c) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
});
import { auth } from "./auth";
import { createAPIFileRoute } from "@tanstack/start/api";
export const APIRoute = createAPIFileRoute("/api/$")({
GET: async ({ request }) => {
const session = await auth.api.getSession({
headers: request.headers,
});
},
});
Using plugins
One of Better Auth’s key features is its plugin system — you can add complex auth functionality with just a few lines of code.
Here’s an example using the two-factor authentication plugin:
Configure the server
Import the plugin and add it to the plugins array in your auth instance:import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
// ...rest of your options
plugins: [
twoFactor(),
],
});
Better Auth will now expose two-factor routes and methods on the server. Migrate the database
Plugins often require additional tables. Run the CLI to apply the changes:Or apply the migration directly: Configure the client
Add the matching client plugin to your auth client:import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [
twoFactorClient({
twoFactorPage: "/two-factor", // redirect here if 2FA is required
}),
],
});
Two-factor methods are now available on the client:import { authClient } from "./auth-client";
const enableTwoFactor = async () => {
const data = await authClient.twoFactor.enable({
password, // user's current password is required
});
};
const disableTwoFactor = async () => {
const data = await authClient.twoFactor.disable({
password,
});
};
const verifyTOTP = async () => {
const data = await authClient.twoFactor.verifyTOTP({
code: "123456", // code entered by the user
/**
* If trusted, the user won't need to pass 2FA again
* on the same device.
*/
trustDevice: true,
});
};