# basic-usage: Basic Usage
URL: /docs/basic-usage
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/basic-usage.mdx
Getting started with Better Auth
***
title: Basic Usage
description: Getting started with Better Auth
---------------------------------------------
Better Auth provides built-in authentication support for:
* **Email and password**
* **Social provider (Google, GitHub, Apple, and more)**
But also can easily be extended using plugins, such as: [username](/docs/plugins/username), [magic link](/docs/plugins/magic-link), [passkey](/docs/plugins/passkey), [email-otp](/docs/plugins/email-otp), and more.
## Email & Password
To enable email and password authentication:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
emailAndPassword: { // [!code highlight]
enabled: true // [!code highlight]
} // [!code highlight]
})
```
### Sign Up
To sign up a user you need to call the client method `signUp.email` with the user's information.
```ts title="sign-up.ts"
import { authClient } from "@/lib/auth-client"; //import the auth client // [!code highlight]
const { data, error } = await authClient.signUp.email({
email, // user email address
password, // user password -> min 8 characters by default
name, // user display name
image, // User image URL (optional)
callbackURL: "/dashboard" // A URL to redirect to after the user verifies their email (optional)
}, {
onRequest: (ctx) => {
//show loading
},
onSuccess: (ctx) => {
//redirect to the dashboard or sign in page
},
onError: (ctx) => {
// display the error message
alert(ctx.error.message);
},
});
```
By default, the users are automatically signed in after they successfully sign up. To disable this behavior you can set `autoSignIn` to `false`.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
autoSignIn: false //defaults to true // [!code highlight]
},
})
```
### Sign In
To sign a user in, you can use the `signIn.email` function provided by the client.
```ts title="sign-in"
const { data, error } = await authClient.signIn.email({
/**
* The user email
*/
email,
/**
* The user password
*/
password,
/**
* A URL to redirect to after the user verifies their email (optional)
*/
callbackURL: "/dashboard",
/**
* remember the user session after the browser is closed.
* @default true
*/
rememberMe: false
}, {
//callbacks
})
```
Always invoke client methods from the client side. Don't call them from the server.
### Server-Side Authentication
To authenticate a user on the server, you can use the `auth.api` methods.
```ts title="server.ts"
import { auth } from "./auth"; // path to your Better Auth server instance
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. But for frameworks like Next.js we provide [a plugin](/docs/integrations/next#server-action-cookies) to handle this automatically
## Social Sign-On
Better Auth supports multiple social providers, including Google, GitHub, Apple, Discord, and more. To use a social provider, you need to configure the ones you need in the `socialProviders` option on your `auth` object.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: { // [!code highlight]
github: { // [!code highlight]
clientId: process.env.GITHUB_CLIENT_ID!, // [!code highlight]
clientSecret: process.env.GITHUB_CLIENT_SECRET!, // [!code highlight]
} // [!code highlight]
}, // [!code highlight]
})
```
### Sign in with social providers
To sign in using a social provider you need to call `signIn.social`. It takes an object with the following properties:
```ts title="sign-in.ts"
import { authClient } from "@/lib/auth-client"; //import the auth client // [!code highlight]
await authClient.signIn.social({
/**
* The social provider ID
* @example "github", "google", "apple"
*/
provider: "github",
/**
* A URL to redirect after the user authenticates with the provider
* @default "/"
*/
callbackURL: "/dashboard",
/**
* A URL to redirect if an error occurs during the sign in process
*/
errorCallbackURL: "/error",
/**
* A URL to redirect if the user is newly registered
*/
newUserCallbackURL: "/welcome",
/**
* disable the automatic redirect to the provider.
* @default false
*/
disableRedirect: true,
});
```
You can also authenticate using `idToken` or `accessToken` from the social provider instead of redirecting the user to the provider's site. See social providers documentation for more details.
## Signout
To signout a user, you can use the `signOut` function provided by the client.
```ts title="user-card.tsx"
await authClient.signOut();
```
you can pass `fetchOptions` to redirect onSuccess
```ts title="user-card.tsx"
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login"); // redirect to login page
},
},
});
```
## Session
Once a user is signed in, you'll want to access the user session. Better Auth allows you to easily access the session data from both the server and client sides.
### Client Side
#### Use Session
Better Auth provides a `useSession` hook to easily access session data on the client side. This hook is implemented using nanostore and has support for each supported framework and vanilla client, ensuring that any changes to the session (such as signing out) are immediately reflected in your UI.
```tsx title="user.tsx"
import { authClient } from "@/lib/auth-client" // import the auth client // [!code highlight]
export function User(){
const { // [!code highlight]
data: session, // [!code highlight]
isPending, //loading state // [!code highlight]
error, //error object // [!code highlight]
refetch //refetch the session
} = authClient.useSession() // [!code highlight]
return (
//...
)
}
```
```vue title="index.vue"
{{ session.data }}
```
```svelte title="user.svelte"
{$session.data?.user.email}
```
```ts title="user.svelte"
import { authClient } from "~/lib/auth-client"; //import the auth client
authClient.useSession.subscribe((value)=>{
//do something with the session //
})
```
```tsx title="user.tsx"
import { authClient } from "~/lib/auth-client"; // [!code highlight]
export default function Home() {
const session = authClient.useSession() // [!code highlight]
return (
{JSON.stringify(session(), null, 2)}
);
}
```
#### Get Session
If you prefer not to use the hook, you can use the `getSession` method provided by the client.
```ts title="user.tsx"
import { authClient } from "@/lib/auth-client" // import the auth client // [!code highlight]
const { data: session, error } = await authClient.getSession()
```
You can also use it with client-side data-fetching libraries like [TanStack Query](https://tanstack.com/query/latest).
### Server Side
The server provides a `session` object that you can use to access the session data. It requires request headers object to be passed to the `getSession` method.
**Example: Using some popular frameworks**
```ts title="server.ts"
import { auth } from "./auth"; // path to your Better Auth server instance
import { headers } from "next/headers";
const session = await auth.api.getSession({
headers: await headers() // you need to pass the headers object.
})
```
```ts title="route.ts"
import { auth } from "lib/auth"; // path to your Better Auth server instance
export async function loader({ request }: LoaderFunctionArgs) {
const session = await auth.api.getSession({
headers: request.headers
})
return json({ session })
}
```
```astro title="index.astro"
---
import { auth } from "./auth";
const session = await auth.api.getSession({
headers: Astro.request.headers,
});
---
```
```ts title="+page.ts"
import { auth } from "./auth";
export async function load({ request }) {
const session = await auth.api.getSession({
headers: request.headers
})
return {
props: {
session
}
}
}
```
```ts title="index.ts"
import { auth } from "./auth";
const app = new Hono();
app.get("/path", async (c) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers
})
});
```
```ts title="server/session.ts"
import { auth } from "~/utils/auth";
export default defineEventHandler((event) => {
const session = await auth.api.getSession({
headers: event.headers,
})
});
```
```ts title="app/routes/api/index.ts"
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
})
},
});
```
For more details check [session-management](/docs/concepts/session-management) documentation.
## Using Plugins
One of the unique features of Better Auth is a plugins ecosystem. It allows you to add complex auth related functionality with small lines of code.
Below is an example of how to add two factor authentication using two factor plugin.
### Server Configuration
To add a plugin, you need to import the plugin and pass it to the `plugins` option of the auth instance. For example, to add two factor authentication, you can use the following code:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
//...rest of the options
plugins: [ // [!code highlight]
twoFactor() // [!code highlight]
] // [!code highlight]
})
```
now two factor related routes and method will be available on the server.
### Migrate Database
After adding the plugin, you'll need to add the required tables to your database. You can do this by running the `migrate` command, or by using the `generate` command to create the schema and handle the migration manually.
generating the schema:
```bash title="terminal"
npx @better-auth/cli generate
```
using the `migrate` command:
```bash title="terminal"
npx @better-auth/cli migrate
```
If you prefer adding the schema manually, you can check the schema required on the [two factor plugin](/docs/plugins/2fa#schema) documentation.
### Client Configuration
Once we're done with the server, we need to add the plugin to the client. To do this, you need to import the plugin and pass it to the `plugins` option of the auth client. For example, to add two factor authentication, you can use the following code:
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins"; // [!code highlight]
const authClient = createAuthClient({
plugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // the page to redirect if a user needs to verify 2nd factor // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
```
now two factor related methods will be available on the client.
```ts title="profile.ts"
import { authClient } from "./auth-client"
const enableTwoFactor = async() => {
const data = await authClient.twoFactor.enable({
password // the user password is required
}) // this will enable two factor
}
const disableTwoFactor = async() => {
const data = await authClient.twoFactor.disable({
password // the user password is required
}) // this will disable two factor
}
const signInWith2Factor = async() => {
const data = await authClient.signIn.email({
//...
})
//if the user has two factor enabled, it will redirect to the two factor page
}
const verifyTOTP = async() => {
const data = await authClient.twoFactor.verifyTOTP({
code: "123456", // the code entered by the user
/**
* If the device is trusted, the user won't
* need to pass 2FA again on the same device
*/
trustDevice: true
})
}
```
Next step: See the two factor plugin documentation.
# comparison: Comparison
URL: /docs/comparison
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/comparison.mdx
Comparison of Better Auth versus over other auth libraries and services.
***
title: Comparison
description: Comparison of Better Auth versus over other auth libraries and services.
-------------------------------------------------------------------------------------
>
Comparison is the thief of joy.
Here are non detailed reasons why you may want to use Better Auth over other auth libraries and services.
### vs Other Auth Libraries
* **Framework agnostic** - Works with any framework, not just specific ones
* **Advanced features built-in** - 2FA, multi-tenancy, multi-session, rate limiting, and many more
* **Plugin system** - Extend functionality without forking or complex workarounds
* **Full control** - Customize auth flows exactly how you want
### vs Self-Hosted Auth Servers
* **No separate infrastructure** - Runs in your app, users stay in your database
* **Zero server maintenance** - No auth servers to deploy, monitor, or update
* **Complete feature set** - Everything you need without the operational overhead
### vs Managed Auth Services
* **Keep your data** - Users stay in your database, not a third-party service
* **No per-user costs** - Scale without worrying about auth billing
* **Single source of truth** - All user data in one place
### vs Rolling Your Own
* **Security handled** - Battle-tested auth flows and security practices
* **Focus on your product** - Spend time on features that matter to your business
* **Plugin extensibility** - Add custom features without starting from scratch
# installation: Installation
URL: /docs/installation
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/installation.mdx
Learn how to configure Better Auth in your project.
***
title: Installation
description: Learn how to configure Better Auth in your project.
----------------------------------------------------------------
### Install the Package
Let's start by adding Better Auth to your project:
npm
pnpm
yarn
bun
```bash
npm install better-auth
```
```bash
pnpm add better-auth
```
```bash
yarn add better-auth
```
```bash
bun add better-auth
```
If you're using a separate client and server setup, make sure to install Better Auth in both parts of your project.
### Set Environment Variables
Create a `.env` file in the root of your project and add the following environment variables:
1. **Secret Key**
Random value used by the library for encryption and generating hashes. **You can generate one using the button below** or you can use something like openssl.
```txt title=".env"
BETTER_AUTH_SECRET=
```
2. **Set Base URL**
```txt title=".env"
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app
```
### Create A Better Auth Instance
Create a file named `auth.ts` in one of these locations:
* Project root
* `lib/` folder
* `utils/` folder
You can also nest any of these folders under `src/`, `app/` or `server/` folder. (e.g. `src/lib/auth.ts`, `app/lib/auth.ts`).
And in this file, import Better Auth and create your auth instance. Make sure to export the auth instance with the variable name `auth` or as a `default` export.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...
});
```
### Configure Database
Better Auth requires a database to store user data.
You can easily configure Better Auth to use SQLite, PostgreSQL, or MySQL, and more!
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import Database from "better-sqlite3";
export const auth = betterAuth({
database: new Database("./sqlite.db"),
})
```
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
// connection options
}),
})
```
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { createPool } from "mysql2/promise";
export const auth = betterAuth({
database: createPool({
// connection options
}),
})
```
Alternatively, if you prefer to use an ORM, you can use one of the built-in adapters.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db"; // your drizzle instance
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite"
}),
});
```
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
// If your Prisma file is located elsewhere, you can change the path
import { PrismaClient } from "@/generated/prisma";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "sqlite", // or "mysql", "postgresql", ...etc
}),
});
```
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { client } from "@/db"; // your mongodb client
export const auth = betterAuth({
database: mongodbAdapter(client),
});
```
If your database is not listed above, check out our other supported
[databases](/docs/adapters/other-relational-databases) for more information,
or use one of the supported ORMs.
### Create Database Tables
Better Auth includes a CLI tool to help manage the schema required by the library.
* **Generate**: This command generates an ORM schema or SQL migration file.
If you're using Kysely, you can apply the migration directly with `migrate` command below. Use `generate` only if you plan to apply the migration manually.
```bash title="Terminal"
npx @better-auth/cli generate
```
* **Migrate**: This command creates the required tables directly in the database. (Available only for the built-in Kysely adapter)
```bash title="Terminal"
npx @better-auth/cli migrate
```
see the [CLI documentation](/docs/concepts/cli) for more information.
If you instead want to create the schema manually, you can find the core schema required in the [database section](/docs/concepts/database#core-schema).
### Authentication Methods
Configure the authentication methods you want to use. Better Auth comes with built-in support for email/password, and social sign-on providers.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
emailAndPassword: {
// [!code highlight]
enabled: true, // [!code highlight]
}, // [!code highlight]
socialProviders: {
// [!code highlight]
github: {
// [!code highlight]
clientId: process.env.GITHUB_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.GITHUB_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
}, // [!code highlight]
});
```
You can use even more authentication methods like [passkey](/docs/plugins/passkey), [username](/docs/plugins/username), [magic link](/docs/plugins/magic-link) and more through plugins.
### Mount Handler
To handle API requests, you need to set up a route handler on your server.
Create a new file or route in your framework's designated catch-all route handler. This route should handle requests for the path `/api/auth/*` (unless you've configured a different base path).
Better Auth supports any backend framework with standard Request and Response
objects and offers helper functions for popular frameworks.
```ts title="/app/api/auth/[...all]/route.ts"
import { auth } from "@/lib/auth"; // path to your auth file
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
```
```ts title="/server/api/auth/[...all].ts"
import { auth } from "~/utils/auth"; // path to your auth file
export default defineEventHandler((event) => {
return auth.handler(toWebRequest(event));
});
```
```ts title="hooks.server.ts"
import { auth } from "$lib/auth"; // path to your auth file
import { svelteKitHandler } from "better-auth/svelte-kit";
import { building } from '$app/environment'
export async function handle({ event, resolve }) {
return svelteKitHandler({ event, resolve, auth, building });
}
```
```ts title="/app/routes/api.auth.$.ts"
import { auth } from '~/lib/auth.server' // Adjust the path as necessary
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"
export async function loader({ request }: LoaderFunctionArgs) {
return auth.handler(request)
}
export async function action({ request }: ActionFunctionArgs) {
return auth.handler(request)
}
```
```ts title="/routes/api/auth/*all.ts"
import { auth } from "~/lib/auth"; // path to your auth file
import { toSolidStartHandler } from "better-auth/solid-start";
export const { GET, POST } = toSolidStartHandler(auth);
```
```ts title="src/index.ts"
import { Hono } from "hono";
import { auth } from "./auth"; // path to your auth file
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));
serve(app);
```
ExpressJS v5 introduced breaking changes to route path matching by switching to `path-to-regexp@6`. Wildcard routes like `*` should now be written using the new named syntax, e.g. `/{*any}`, to properly capture catch-all patterns. This ensures compatibility and predictable behavior across routing scenarios.
See the [Express v5 migration guide](https://expressjs.com/en/guide/migrating-5.html) for details.
As a result, the implementation in ExpressJS v5 should look like this:
```ts
app.all('/api/auth/{*any}', toNodeHandler(auth));
```
*The name any is arbitrary and can be replaced with any identifier you prefer.*
```ts title="server.ts"
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";
const app = express();
const port = 8000;
app.all("/api/auth/*", toNodeHandler(auth));
// Mount express json middleware after Better Auth handler
// or only apply it to routes that don't interact with Better Auth
app.use(express.json());
app.listen(port, () => {
console.log(`Better Auth app listening on port ${port}`);
});
```
This will also work for any other node server framework like express, fastify, hapi, etc., but may require some modifications. See [fastify guide](/docs/integrations/fastify). Note that CommonJS (cjs) isn't supported.
```ts title="/pages/api/auth/[...all].ts"
import type { APIRoute } from "astro";
import { auth } from "@/auth"; // path to your auth file
export const GET: APIRoute = async (ctx) => {
return auth.handler(ctx.request);
};
export const POST: APIRoute = async (ctx) => {
return auth.handler(ctx.request);
};
```
```ts
import { Elysia, Context } from "elysia";
import { auth } from "./auth";
const betterAuthView = (context: Context) => {
const BETTER_AUTH_ACCEPT_METHODS = ["POST", "GET"]
// validate request method
if(BETTER_AUTH_ACCEPT_METHODS.includes(context.request.method)) {
return auth.handler(context.request);
} else {
context.error(405)
}
}
const app = new Elysia().all("/api/auth/*", betterAuthView).listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
```
```ts title="src/routes/api/auth/$.ts"
import { auth } from '~/lib/server/auth'
import { createServerFileRoute } from '@tanstack/react-start/server'
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
GET: ({ request }) => {
return auth.handler(request)
},
POST: ({ request }) => {
return auth.handler(request)
},
});
```
```ts title="app/api/auth/[...all]+api.ts"
import { auth } from '@/lib/server/auth'; // path to your auth file
const handler = auth.handler;
export { handler as GET, handler as POST };
```
### Create Client Instance
The client-side library helps you interact with the auth server. Better Auth comes with a client for all the popular web frameworks, including vanilla JavaScript.
1. Import `createAuthClient` from the package for your framework (e.g., "better-auth/react" for React).
2. Call the function to create your client.
3. Pass the base URL of your auth server. (If the auth server is running on the same domain as your client, you can skip this step.)
If you're using a different base path other than `/api/auth` make sure to pass
the whole URL including the path. (e.g.
`http://localhost:3000/custom-path/auth`)
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/client"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */ // [!code highlight]
baseURL: "http://localhost:3000" // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */ // [!code highlight]
baseURL: "http://localhost:3000" // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/vue"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */ // [!code highlight]
baseURL: "http://localhost:3000" // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/svelte"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */ // [!code highlight]
baseURL: "http://localhost:3000" // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/solid"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */ // [!code highlight]
baseURL: "http://localhost:3000" // [!code highlight]
})
```
Tip: You can also export specific methods if you prefer:
```ts
export const { signIn, signUp, useSession } = createAuthClient()
```
### 🎉 That's it!
That's it! You're now ready to use better-auth in your application. Continue to [basic usage](/docs/basic-usage) to learn how to use the auth instance to sign in users.
# introduction: Introduction
URL: /docs/introduction
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/introduction.mdx
Introduction to Better Auth.
***
title: Introduction
description: Introduction to Better Auth.
-----------------------------------------
Better Auth is a framework-agnostic authentication and authorization framework for TypeScript. It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities. Whether you need 2FA, multi-tenancy, multi-session support, or even enterprise features like SSO, it lets you focus on building your application instead of reinventing the wheel.
## Why Better Auth?
*Authentication in the TypeScript ecosystem has long been a half-solved problem. Other open-source libraries often require a lot of additional code for anything beyond basic authentication features. Rather than just pushing third-party services as the solution, I believe we can do better as a community—hence, Better Auth.*
## Features
Better Auth aims to be the most comprehensive auth library. It provides a wide range of features out of the box and allows you to extend it with plugins. Here are some of the features:
...and much more!
## LLMs.txt
Better Auth provides an LLMs.txt file that helps AI models understand how to interact with your authentication system. You can find it at [https://better-auth.com/llms.txt](https://better-auth.com/llms.txt).
# adapters: Community Adapters
URL: /docs/adapters/community-adapters
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/community-adapters.mdx
Integrate Better Auth with community made database adapters.
***
title: Community Adapters
description: Integrate Better Auth with community made database adapters.
-------------------------------------------------------------------------
This page showcases a list of recommended community made database adapters.
We encourage you to create any missing database adapters and maybe get added to the list!
| Adapter | Database Dialect | Author |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [convex-better-auth](https://github.com/get-convex/better-auth) | [Convex Database](https://www.convex.dev/) | [erquhart](https://github.com/erquhart) |
| [surreal-better-auth](https://github.com/oskar-gmerek/surreal-better-auth) | [SurrealDB](https://surrealdb.com/) | Oskar Gmerek |
| [surrealdb-better-auth](https://github.com/Necmttn/surrealdb-better-auth) | [Surreal Database](https://surrealdb.com/) | [Necmttn](https://github.com/Necmttn) |
| [better-auth-surrealdb](https://github.com/msanchezdev/better-auth-surrealdb) | [Surreal Database](https://surrealdb.com/) | [msanchezdev](https://github.com/msanchezdev) |
| [payload-better-auth](https://github.com/ForrestDevs/payload-better-auth/tree/main/packages/db-adapter) | [Payload CMS](https://payloadcms.com/) | [forrestdevs](https://github.com/forrestdevs) |
| [@ronin/better-auth](https://github.com/ronin-co/better-auth) | [RONIN](https://ronin.co) | [ronin-co](https://github.com/ronin-co) |
| [better-auth-instantdb](https://github.com/daveyplate/better-auth-instantdb) | [InstantDB](https://www.instantdb.com/) | [daveycodez](https://github.com/daveycodez) |
| [@nerdfolio/remult-better-auth](https://github.com/nerdfolio/remult-better-auth) | [Remult](https://remult.dev/) | [Tai Vo](https://github.com/taivo) |
# adapters: Drizzle ORM Adapter
URL: /docs/adapters/drizzle
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/drizzle.mdx
Integrate Better Auth with Drizzle ORM.
***
title: Drizzle ORM Adapter
description: Integrate Better Auth with Drizzle ORM.
----------------------------------------------------
Drizzle ORM is a powerful and flexible ORM for Node.js and TypeScript. It provides a simple and intuitive API for working with databases, and supports a wide range of databases including MySQL, PostgreSQL, SQLite, and more.
Read more here: [Drizzle ORM](https://orm.drizzle.team/).
## Example Usage
Make sure you have Drizzle installed and configured.
Then, you can use the Drizzle adapter to connect to your database.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./database.ts";
export const auth = betterAuth({
database: drizzleAdapter(db, {
// [!code highlight]
provider: "sqlite", // or "pg" or "mysql" // [!code highlight]
}), // [!code highlight]
//... the rest of your config
});
```
## Schema generation & migration
The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate
your database schema based on your Better Auth configuration and plugins.
To generate the schema required by Better Auth, run the following command:
```bash title="Schema Generation"
npx @better-auth/cli@latest generate
```
To generate and apply the migration, run the following commands:
```bash title="Schema Migration"
npx drizzle-kit generate # generate the migration file
npx drizzle-kit migrate # apply the migration
```
## Additional Information
The Drizzle adapter expects the schema you define to match the table names. For example, if your Drizzle schema maps the `user` table to `users`, you need to manually pass the schema and map it to the user table.
```ts
import { betterAuth } from "better-auth";
import { db } from "./drizzle";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { schema } from "./schema";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite", // or "pg" or "mysql"
schema: {
...schema,
user: schema.users,
},
}),
});
```
If all your tables are using plural form, you can just pass the `usePlural` option:
```ts
export const auth = betterAuth({
database: drizzleAdapter(db, {
...
usePlural: true,
}),
});
```
If you're looking for performance improvements or tips, take a look at our guide to performance optimizations.
# adapters: MongoDB Adapter
URL: /docs/adapters/mongo
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/mongo.mdx
Integrate Better Auth with MongoDB.
***
title: MongoDB Adapter
description: Integrate Better Auth with MongoDB.
------------------------------------------------
MongoDB is a popular NoSQL database that is widely used for building scalable and flexible applications. It provides a flexible schema that allows for easy data modeling and querying.
Read more here: [MongoDB](https://www.mongodb.com/).
## Example Usage
Make sure you have MongoDB installed and configured.
Then, you can use the mongodb adapter.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { MongoClient } from "mongodb";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
const client = new MongoClient("mongodb://localhost:27017/database");
const db = client.db();
export const auth = betterAuth({
database: mongodbAdapter(db, {
// Optional: if you don't provide a client, database transactions won't be enabled.
client
}),
});
```
## Schema generation & migration
For MongoDB, we don't need to generate or migrate the schema.
# adapters: MS SQL
URL: /docs/adapters/mssql
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/mssql.mdx
Integrate Better Auth with MS SQL.
***
title: MS SQL
description: Integrate Better Auth with MS SQL.
-----------------------------------------------
Microsoft SQL Server is a relational database management system developed by Microsoft, designed for enterprise-level data storage, management, and analytics with robust security and scalability features.
Read more [here](https://en.wikipedia.org/wiki/Microsoft_SQL_Server).
## Example Usage
Make sure you have MS SQL installed and configured.
Then, you can connect it straight into Better Auth.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { MssqlDialect } from "kysely";
import * as Tedious from 'tedious'
import * as Tarn from 'tarn'
const dialect = new MssqlDialect({
tarn: {
...Tarn,
options: {
min: 0,
max: 10,
},
},
tedious: {
...Tedious,
connectionFactory: () => new Tedious.Connection({
authentication: {
options: {
password: 'password',
userName: 'username',
},
type: 'default',
},
options: {
database: 'some_db',
port: 1433,
trustServerCertificate: true,
},
server: 'localhost',
}),
},
})
export const auth = betterAuth({
database: {
dialect,
type: "mssql"
}
});
```
For more information, read Kysely's documentation to the [MssqlDialect](https://kysely-org.github.io/kysely-apidoc/classes/MssqlDialect.html).
## Schema generation & migration
The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate
your database schema based on your Better Auth configuration and plugins.
MS SQL Schema Generation
MS SQL Schema Migration
✅ Supported
✅ Supported
```bash title="Schema Generation"
npx @better-auth/cli@latest generate
```
```bash title="Schema Migration"
npx @better-auth/cli@latest migrate
```
## Additional Information
MS SQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (Read more here)
If you're looking for performance improvements or tips, take a look at our guide to performance optimizations.
# adapters: MySQL
URL: /docs/adapters/mysql
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/mysql.mdx
Integrate Better Auth with MySQL.
***
title: MySQL
description: Integrate Better Auth with MySQL.
----------------------------------------------
MySQL is a popular open-source relational database management system (RDBMS) that is widely used for building web applications and other types of software. It provides a flexible and scalable database solution that allows for efficient storage and retrieval of data.
Read more here: [MySQL](https://www.mysql.com/).
## Example Usage
Make sure you have MySQL installed and configured.
Then, you can connect it straight into Better Auth.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { createPool } from "mysql2/promise";
export const auth = betterAuth({
database: createPool({
host: "localhost",
user: "root",
password: "password",
database: "database",
}),
});
```
For more information, read Kysely's documentation to the
[MySQLDialect](https://kysely-org.github.io/kysely-apidoc/classes/MysqlDialect.html).
## Schema generation & migration
The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate
your database schema based on your Better Auth configuration and plugins.
MySQL Schema Generation
MySQL Schema Migration
✅ Supported
✅ Supported
```bash title="Schema Generation"
npx @better-auth/cli@latest generate
```
```bash title="Schema Migration"
npx @better-auth/cli@latest migrate
```
## Additional Information
MySQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (Read more here)
If you're looking for performance improvements or tips, take a look at our guide to performance optimizations.
# adapters: Other Relational Databases
URL: /docs/adapters/other-relational-databases
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/other-relational-databases.mdx
Integrate Better Auth with other relational databases.
***
title: Other Relational Databases
description: Integrate Better Auth with other relational databases.
-------------------------------------------------------------------
Better Auth supports a wide range of database dialects out of the box thanks to Kysely.
Any dialect supported by Kysely can be utilized with Better Auth, including capabilities for generating and migrating database schemas through the CLI.
## Core Dialects
* [MySQL](/docs/adapters/mysql)
* [SQLite](/docs/adapters/sqlite)
* [PostgreSQL](/docs/adapters/postgresql)
* [MS SQL](/docs/adapters/mssql)
## Kysely Organization Dialects
* [Postgres.js](https://github.com/kysely-org/kysely-postgres-js)
* [SingleStore Data API](https://github.com/kysely-org/kysely-singlestore)
## Kysely Community dialects
* [PlanetScale Serverless Driver](https://github.com/depot/kysely-planetscale)
* [Cloudflare D1](https://github.com/aidenwallis/kysely-d1)
* [AWS RDS Data API](https://github.com/serverless-stack/kysely-data-api)
* [SurrealDB](https://github.com/igalklebanov/kysely-surrealdb)
* [Neon](https://github.com/seveibar/kysely-neon)
* [Xata](https://github.com/xataio/client-ts/tree/main/packages/plugin-client-kysely)
* [AWS S3 Select](https://github.com/igalklebanov/kysely-s3-select)
* [libSQL/sqld](https://github.com/libsql/kysely-libsql)
* [Fetch driver](https://github.com/andersgee/kysely-fetch-driver)
* [SQLite WASM](https://github.com/DallasHoff/sqlocal)
* [Deno SQLite](https://gitlab.com/soapbox-pub/kysely-deno-sqlite)
* [TiDB Cloud Serverless Driver](https://github.com/tidbcloud/kysely)
* [Capacitor SQLite Kysely](https://github.com/DawidWetzler/capacitor-sqlite-kysely)
* [BigQuery](https://github.com/maktouch/kysely-bigquery)
* [Clickhouse](https://github.com/founderpathcom/kysely-clickhouse)
* [PGLite](https://github.com/czeidler/kysely-pglite-dialect)
You can see the full list of supported Kysely dialects{" "}
here.
# adapters: PostgreSQL
URL: /docs/adapters/postgresql
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/postgresql.mdx
Integrate Better Auth with PostgreSQL.
***
title: PostgreSQL
description: Integrate Better Auth with PostgreSQL.
---------------------------------------------------
PostgreSQL is a powerful, open-source relational database management system known for its advanced features, extensibility, and support for complex queries and large datasets.
Read more [here](https://www.postgresql.org/).
## Example Usage
Make sure you have PostgreSQL installed and configured.
Then, you can connect it straight into Better Auth.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: "postgres://user:password@localhost:5432/database",
}),
});
```
For more information, read Kysely's documentation to the
[PostgresDialect](https://kysely-org.github.io/kysely-apidoc/classes/PostgresDialect.html).
## Schema generation & migration
The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate
your database schema based on your Better Auth configuration and plugins.
PostgreSQL Schema Generation
PostgreSQL Schema Migration
✅ Supported
✅ Supported
```bash title="Schema Generation"
npx @better-auth/cli@latest generate
```
```bash title="Schema Migration"
npx @better-auth/cli@latest migrate
```
## Additional Information
PostgreSQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (Read more here)
If you're looking for performance improvements or tips, take a look at our guide to performance optimizations.
# adapters: Prisma
URL: /docs/adapters/prisma
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/prisma.mdx
Integrate Better Auth with Prisma.
***
title: Prisma
description: Integrate Better Auth with Prisma.
-----------------------------------------------
Prisma ORM is an open-source database toolkit that simplifies database access and management in applications by providing a type-safe query builder and an intuitive data modeling interface.
Read more [here](https://www.prisma.io/).
## Example Usage
Make sure you have Prisma installed and configured.
Then, you can use the Prisma adapter to connect to your database.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "sqlite",
}),
});
```
If you have configured a custom output directory in your `schema.prisma` file (e.g., `output = "../src/generated/prisma"`), make sure to import the Prisma client from that location instead of `@prisma/client`. Learn more about custom output directories in the [Prisma documentation](https://www.prisma.io/docs/guides/nextjs#21-install-prisma-orm-and-create-your-first-models).
## Schema generation & migration
The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate
your database schema based on your Better Auth configuration and plugins.
Prisma Schema Generation
Prisma Schema Migration
✅ Supported
❌ Not Supported
```bash title="Schema Generation"
npx @better-auth/cli@latest generate
```
## Additional Information
If you're looking for performance improvements or tips, take a look at our guide to performance optimizations.
# adapters: SQLite
URL: /docs/adapters/sqlite
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/sqlite.mdx
Integrate Better Auth with SQLite.
***
title: SQLite
description: Integrate Better Auth with SQLite.
-----------------------------------------------
SQLite is a lightweight, serverless, self-contained SQL database engine that is widely used for local data storage in applications.
Read more [here.](https://www.sqlite.org/)
## Example Usage
Better Auth supports multiple SQLite drivers. Choose the one that best fits your environment:
### Better-SQLite3 (Recommended)
The most popular and stable SQLite driver for Node.js:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import Database from "better-sqlite3";
export const auth = betterAuth({
database: new Database("database.sqlite"),
});
```
For more information, read Kysely's documentation to the
[SqliteDialect](https://kysely-org.github.io/kysely-apidoc/classes/SqliteDialect.html).
### Node.js Built-in SQLite (Experimental)
The `node:sqlite` module is still experimental and may change at any time. It requires Node.js 22.5.0 or later.
Starting from Node.js 22.5.0, you can use the built-in [SQLite](https://nodejs.org/api/sqlite.html) module:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { DatabaseSync } from "node:sqlite";
export const auth = betterAuth({
database: new DatabaseSync("database.sqlite"),
});
```
To run your application with Node.js SQLite:
```bash
node your-app.js
```
### Bun Built-in SQLite
You can also use the built-in [SQLite](https://bun.com/docs/api/sqlite) module in Bun, which is similar to the Node.js version:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { Database } from "bun:sqlite";
export const auth = betterAuth({
database: new Database("database.sqlite"),
});
```
## Schema generation & migration
The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate
your database schema based on your Better Auth configuration and plugins.
SQLite Schema Generation
SQLite Schema Migration
✅ Supported
✅ Supported
```bash title="Schema Generation"
npx @better-auth/cli@latest generate
```
```bash title="Schema Migration"
npx @better-auth/cli@latest migrate
```
## Additional Information
SQLite is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (Read more here)
If you're looking for performance improvements or tips, take a look at our guide to performance optimizations.
# authentication: Apple
URL: /docs/authentication/apple
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/apple.mdx
Apple provider setup and usage.
***
title: Apple
description: Apple provider setup and usage.
--------------------------------------------
### Get your OAuth credentials
To use Apple sign in, you need a client ID and client secret. You can get them from the [Apple Developer Portal](https://developer.apple.com/account/resources/authkeys/list).
You will need an active **Apple Developer account** to access the developer portal and generate these credentials.
Follow these steps to set up your App ID, Service ID, and generate the key needed for your client secret:
1. **Navigate to Certificates, Identifiers & Profiles:**
In the Apple Developer Portal, go to the "Certificates, Identifiers & Profiles" section.
2. **Create an App ID:**
* Go to the `Identifiers` tab.
* Click the `+` icon next to Identifiers.
* Select `App IDs`, then click `Continue`.
* Select `App` as the type, then click `Continue`.
* **Description:** Enter a name for your app (e.g., "My Awesome App"). This name may be displayed to users when they sign in.
* **Bundle ID:** Set a bundle ID. The recommended format is a reverse domain name (e.g., `com.yourcompany.yourapp`). Using a suffix like `.ai` (for app identifier) can help with organization but is not required (e.g., `com.yourcompany.yourapp.ai`).
* Scroll down to **Capabilities**. Select the checkbox for `Sign In with Apple`.
* Click `Continue`, then `Register`.
3. **Create a Service ID:**
* Go back to the `Identifiers` tab.
* Click the `+` icon.
* Select `Service IDs`, then click `Continue`.
* **Description:** Enter a description for this service (e.g., your app name again).
* **Identifier:** Set a unique identifier for the service. Use a reverse domain format, distinct from your App ID (e.g., `com.yourcompany.yourapp.si`, where `.si` indicates service identifier - this is for your organization and not required). **This Service ID will be your `clientId`.**
* Click `Continue`, then `Register`.
4. **Configure the Service ID:**
* Find the Service ID you just created in the `Identifiers` list and click on it.
* Check the `Sign In with Apple` capability, then click `Configure`.
* Under **Primary App ID**, select the App ID you created earlier (e.g., `com.yourcompany.yourapp.ai`).
* Under **Domains and Subdomains**, list all the root domains you will use for Sign In with Apple (e.g., `example.com`, `anotherdomain.com`).
* Under **Return URLs**, enter the callback URL. `https://yourdomain.com/api/auth/callback/apple`. Add all necessary return URLs.
* Click `Next`, then `Done`.
* Click `Continue`, then `Save`.
5. **Create a Client Secret Key:**
* Go to the `Keys` tab.
* Click the `+` icon to create a new key.
* **Key Name:** Enter a name for the key (e.g., "Sign In with Apple Key").
* Scroll down and select the checkbox for `Sign In with Apple`.
* Click the `Configure` button next to `Sign In with Apple`.
* Select the **Primary App ID** you created earlier.
* Click `Save`, then `Continue`, then `Register`.
* **Download the Key:** Immediately download the `.p8` key file. **This file is only available for download once.** Note the Key ID (available on the Keys page after creation) and your Team ID (available in your Apple Developer Account settings).
6. **Generate the Client Secret (JWT):**
Apple requires a JSON Web Token (JWT) to be generated dynamically using the downloaded `.p8` key, the Key ID, and your Team ID. This JWT serves as your `clientSecret`.
You can use the guide below from [Apple's documentation](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret) to understand how to generate this client secret. You can also use our built in generator [below](#generate-apple-client-secret-jwt) to generate the client secret JWT required for 'Sign in with Apple'.
### Configure the provider
To configure the provider, you need to add it to the `socialProviders` option of the auth instance.
You also need to add `https://appleid.apple.com` to the `trustedOrigins` array in your auth instance configuration to allow communication with Apple's authentication servers.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
apple: { // [!code highlight]
clientId: process.env.APPLE_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.APPLE_CLIENT_SECRET as string, // [!code highlight]
// Optional
appBundleIdentifier: process.env.APPLE_APP_BUNDLE_IDENTIFIER as string, // [!code highlight]
}, // [!code highlight]
},
// Add appleid.apple.com to trustedOrigins for Sign In with Apple flows
trustedOrigins: ["https://appleid.apple.com"], // [!code highlight]
})
```
On native iOS, it doesn't use the service ID but the app ID (bundle ID) as client ID, so if using the service ID as `clientId` in `signIn.social` with `idToken`, it throws an error: `JWTClaimValidationFailed: unexpected "aud" claim value`. So you need to provide the `appBundleIdentifier` when you want to sign in with Apple using the ID Token.
## Usage
### Sign In with Apple
To sign in with Apple, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `apple`.
```ts title="auth-client.ts" /
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "apple"
})
}
```
### Sign In with Apple With ID Token
To sign in with Apple using the ID Token, you can use the `signIn.social` function to pass the ID Token.
This is useful when you have the ID Token from Apple on the client-side and want to use it to sign in on the server.
If ID token is provided no redirection will happen, and the user will be signed in directly.
```ts title="auth-client.ts"
await authClient.signIn.social({
provider: "apple",
idToken: {
token: // Apple ID Token,
nonce: // Nonce (optional)
accessToken: // Access Token (optional)
}
})
```
## Generate Apple Client Secret (JWT)
# authentication: Atlassian
URL: /docs/authentication/atlassian
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/atlassian.mdx
Atlassian provider setup and usage.
***
title: Atlassian
description: Atlassian provider setup and usage.
------------------------------------------------
### Get your Credentials
1. Sign in to your Atlassian account and go to the [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/)
2. Click "Create new app"
3. Fill out the app details
4. Configure your redirect URI (e.g., `https://yourdomain.com/api/auth/callback/atlassian`)
5. Note your Client ID and Client Secret
* The default scope is `read:jira-user` and `offline_access`. For additional scopes, refer to the [Atlassian OAuth documentation](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/).
Make sure to set the redirect URI to match your application's callback URL. If you change the base path of the auth routes, you should update the redirect URI accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
atlassian: { // [!code highlight]
clientId: process.env.ATLASSIAN_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.ATLASSIAN_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Atlassian
To sign in with Atlassian, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `atlassian`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "atlassian"
})
}
```
For more information about Atlassian's OAuth scopes and API capabilities, refer to the [official Atlassian OAuth 2.0 (3LO) apps documentation](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/).
# authentication: Cognito
URL: /docs/authentication/cognito
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/cognito.mdx
Amazon Cognito provider setup and usage.
***
title: Cognito
description: Amazon Cognito provider setup and usage.
-----------------------------------------------------
### Get your Cognito Credentials
To integrate with Cognito, you need to set up a **User Pool** and an **App client** in the [Amazon Cognito Console](https://console.aws.amazon.com/cognito/).
Follow these steps:
1. Go to the **Cognito Console** and create a **User Pool**.
2. Under **App clients**, create a new **App client** (note the Client ID and Client Secret if enabled).
3. Go to **Domain** and set a Cognito Hosted UI domain (e.g., `your-app.auth.us-east-1.amazoncognito.com`).
4. In **App client settings**, enable:
* Allowed OAuth flows: `Authorization code grant`
* Allowed OAuth scopes: `openid`, `profile`, `email`
5. Add your callback URL (e.g., `http://localhost:3000/api/auth/callback/cognito`).
* **User Pool is required** for Cognito authentication.
* Make sure the callback URL matches exactly what you configure in Cognito.
### Configure the provider
Configure the `cognito` key in `socialProviders` key of your `auth` instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
cognito: {
clientId: process.env.COGNITO_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.COGNITO_CLIENT_SECRET as string, // [!code highlight]
domain: process.env.COGNITO_DOMAIN as string, // e.g. "your-app.auth.us-east-1.amazoncognito.com" [!code highlight]
region: process.env.COGNITO_REGION as string, // e.g. "us-east-1" [!code highlight]
userPoolId: process.env.COGNITO_USERPOOL_ID as string, // [!code highlight]
},
},
})
```
### Sign In with Cognito
To sign in with Cognito, use the `signIn.social` function from the client.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "cognito"
})
}
```
### Additional Options:
* `scope`: Additional OAuth2 scopes to request (combined with default permissions).
* Default: `"openid" "profile" "email"`
* Common Cognito scopes:
* `openid`: Required for OpenID Connect authentication
* `profile`: Access to basic profile info
* `email`: Access to user’s email
* `phone`: Access to user’s phone number
* `aws.cognito.signin.user.admin`: Grants access to Cognito-specific APIs
* Note: You must configure the scopes in your Cognito App Client settings. [available scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html#token-endpoint-userinfo)
* `getUserInfo`: Custom function to retrieve user information from the Cognito UserInfo endpoint.
For more information about Amazon Cognito's scopes and API capabilities, refer to the [official documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html?utm_source).
# authentication: Discord
URL: /docs/authentication/discord
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/discord.mdx
Discord provider setup and usage.
***
title: Discord
description: Discord provider setup and usage.
----------------------------------------------
### Get your Discord credentials
To use Discord sign in, you need a client ID and client secret. You can get them from the [Discord Developer Portal](https://discord.com/developers/applications).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/discord` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
discord: { // [!code highlight]
clientId: process.env.DISCORD_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.DISCORD_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Discord
To sign in with Discord, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `discord`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "discord"
})
}
```
# authentication: Dropbox
URL: /docs/authentication/dropbox
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/dropbox.mdx
Dropbox provider setup and usage.
***
title: Dropbox
description: Dropbox provider setup and usage.
----------------------------------------------
### Get your Dropbox credentials
To use Dropbox sign in, you need a client ID and client secret. You can get them from the [Dropbox Developer Portal](https://www.dropbox.com/developers). You can Allow "Implicit Grant & PKCE" for the application in the App Console.
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/dropbox` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
If you need deeper dive into Dropbox Authentication, you can check out the [official documentation](https://developers.dropbox.com/oauth-guide).
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
dropbox: { // [!code highlight]
clientId: process.env.DROPBOX_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.DROPBOX_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Dropbox
To sign in with Dropbox, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `dropbox`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "dropbox"
})
}
```
# authentication: Email & Password
URL: /docs/authentication/email-password
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/email-password.mdx
Implementing email and password authentication with Better Auth.
***
title: Email & Password
description: Implementing email and password authentication with Better Auth.
-----------------------------------------------------------------------------
Email and password authentication is a common method used by many applications. Better Auth provides a built-in email and password authenticator that you can easily integrate into your project.
If you prefer username-based authentication, check out the{" "}
username plugin. It extends the
email and password authenticator with username support.
## Enable Email and Password
To enable email and password authentication, you need to set the `emailAndPassword.enabled` option to `true` in the `auth` configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailAndPassword: { // [!code highlight]
enabled: true, // [!code highlight]
}, // [!code highlight]
});
```
If it's not enabled, it'll not allow you to sign in or sign up with email and
password.
## Usage
### Sign Up
To sign a user up, you can use the `signUp.email` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.signUp.email({
name: John Doe,
email: john.doe@example.com,
password: password1234,
image: https://example.com/image.png, // required
callbackURL: https://example.com/callback, // required
});
```
### Server Side
```ts
const data = await auth.api.signUpEmail({
body: {
name: John Doe,
email: john.doe@example.com,
password: password1234,
image: https://example.com/image.png, // required
callbackURL: https://example.com/callback, // required
}
});
```
### Type Definition
```ts
type signUpEmail = {
/**
* The name of the user.
*/
name: string = "John Doe"
/**
* The email address of the user.
*/
email: string = "john.doe@example.com"
/**
* The password of the user. It should be at least 8 characters long and max 128 by default.
*/
password: string = "password1234"
/**
* An optional profile image of the user.
*/
image?: string = "https://example.com/image.png"
/**
* An optional URL to redirect to after the user signs up.
*/
callbackURL?: string = "https://example.com/callback"
}
```
These are the default properties for the sign up email endpoint, however it's possible that with [additional fields](/docs/concepts/typescript#additional-fields) or special plugins you can pass more properties to the endpoint.
### Sign In
To sign a user in, you can use the `signIn.email` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.signIn.email({
email: john.doe@example.com,
password: password1234,
rememberMe, // required
callbackURL: https://example.com/callback, // required
});
```
### Server Side
```ts
const data = await auth.api.signInEmail({
body: {
email: john.doe@example.com,
password: password1234,
rememberMe, // required
callbackURL: https://example.com/callback, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type signInEmail = {
/**
* The email address of the user.
*/
email: string = "john.doe@example.com"
/**
* The password of the user. It should be at least 8 characters long and max 128 by default.
*/
password: string = "password1234"
/**
* If false, the user will be signed out when the browser is closed. (optional) (default: true)
*/
rememberMe?: boolean = true
/**
* An optional URL to redirect to after the user signs in. (optional)
*/
callbackURL?: string = "https://example.com/callback"
}
```
These are the default properties for the sign in email endpoint, however it's possible that with [additional fields](/docs/concepts/typescript#additional-fields) or special plugins you can pass different properties to the endpoint.
### Sign Out
To sign a user out, you can use the `signOut` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.signOut({});
```
### Server Side
```ts
await auth.api.signOut({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type signOut = {
}
```
you can pass `fetchOptions` to redirect onSuccess
```ts title="auth-client.ts"
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login"); // redirect to login page
},
},
});
```
### Email Verification
To enable email verification, you need to pass a function that sends a verification email with a link. The `sendVerificationEmail` function takes a data object with the following properties:
* `user`: The user object.
* `url`: The URL to send to the user which contains the token.
* `token`: A verification token used to complete the email verification.
and a `request` object as the second parameter.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // your email sending function
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ( { user, url, token }, request) => {
await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
});
},
},
});
```
On the client side you can use `sendVerificationEmail` function to send verification link to user. This will trigger the `sendVerificationEmail` function you provided in the `auth` configuration.
Once the user clicks on the link in the email, if the token is valid, the user will be redirected to the URL provided in the `callbackURL` parameter. If the token is invalid, the user will be redirected to the URL provided in the `callbackURL` parameter with an error message in the query string `?error=invalid_token`.
#### Require Email Verification
If you enable require email verification, users must verify their email before they can log in. And every time a user tries to sign in, sendVerificationEmail is called.
This only works if you have sendVerificationEmail implemented and if the user
is trying to sign in with email and password.
```ts title="auth.ts"
export const auth = betterAuth({
emailAndPassword: {
requireEmailVerification: true,
},
});
```
If a user tries to sign in without verifying their email, you can handle the error and show a message to the user.
```ts title="auth-client.ts"
await authClient.signIn.email(
{
email: "email@example.com",
password: "password",
},
{
onError: (ctx) => {
// Handle the error
if (ctx.error.status === 403) {
alert("Please verify your email address");
}
//you can also show the original error message
alert(ctx.error.message);
},
}
);
```
#### Triggering manually Email Verification
You can trigger the email verification manually by calling the `sendVerificationEmail` function.
```ts
await authClient.sendVerificationEmail({
email: "user@email.com",
callbackURL: "/", // The redirect URL after verification
});
```
### Request Password Reset
To allow users to reset a password first you need to provide `sendResetPassword` function to the email and password authenticator. The `sendResetPassword` function takes a data object with the following properties:
* `user`: The user object.
* `url`: The URL to send to the user which contains the token.
* `token`: A verification token used to complete the password reset.
and a `request` object as the second parameter.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // your email sending function
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
sendResetPassword: async ({user, url, token}, request) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
text: `Click the link to reset your password: ${url}`,
});
},
onPasswordReset: async ({ user }, request) => {
// your logic here
console.log(`Password for user ${user.email} has been reset.`);
},
},
});
```
Additionally, you can provide an `onPasswordReset` callback to execute logic after a password has been successfully reset.
Once you configured your server you can call `requestPasswordReset` function to send reset password link to user. If the user exists, it will trigger the `sendResetPassword` function you provided in the auth config.
### Client Side
```ts
const { data, error } = await authClient.requestPasswordReset({
email: john.doe@example.com,
redirectTo: https://example.com/reset-password, // required
});
```
### Server Side
```ts
const data = await auth.api.requestPasswordReset({
body: {
email: john.doe@example.com,
redirectTo: https://example.com/reset-password, // required
}
});
```
### Type Definition
```ts
type requestPasswordReset = {
/**
* The email address of the user to send a password reset email to
*/
email: string = "john.doe@example.com"
/**
* The URL to redirect the user to reset their password. If the token isn't valid or expired, it'll be redirected with a query parameter `?error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?token=VALID_TOKEN
*/
redirectTo?: string = "https://example.com/reset-password"
}
```
When a user clicks on the link in the email, they will be redirected to the reset password page. You can add the reset password page to your app. Then you can use `resetPassword` function to reset the password. It takes an object with the following properties:
* `newPassword`: The new password of the user.
```ts title="auth-client.ts"
const { data, error } = await authClient.resetPassword({
newPassword: "password1234",
token,
});
```
### Client Side
```ts
const { data, error } = await authClient.resetPassword({
newPassword: password1234,
token,
});
```
### Server Side
```ts
const data = await auth.api.resetPassword({
body: {
newPassword: password1234,
token,
}
});
```
### Type Definition
```ts
type resetPassword = {
/**
* The new password to set
*/
newPassword: string = "password1234"
/**
* The token to reset the password
*/
token: string
}
```
### Update password
A user's password isn't stored in the user table. Instead, it's stored in the account table. To change the password of a user, you can use one of the following approaches:
### Client Side
```ts
const { data, error } = await authClient.changePassword({
newPassword: newpassword1234,
currentPassword: oldpassword1234,
revokeOtherSessions, // required
});
```
### Server Side
```ts
const data = await auth.api.changePassword({
body: {
newPassword: newpassword1234,
currentPassword: oldpassword1234,
revokeOtherSessions, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type changePassword = {
/**
* The new password to set
*/
newPassword: string = "newpassword1234"
/**
* The current user password
*/
currentPassword: string = "oldpassword1234"
/**
* When set to true, all other active sessions for this user will be invalidated
*/
revokeOtherSessions?: boolean = true
}
```
### Configuration
**Password**
Better Auth stores passwords inside the `account` table with `providerId` set to `credential`.
**Password Hashing**: Better Auth uses `scrypt` to hash passwords. The `scrypt` algorithm is designed to be slow and memory-intensive to make it difficult for attackers to brute force passwords. OWASP recommends using `scrypt` if `argon2id` is not available. We decided to use `scrypt` because it's natively supported by Node.js.
You can pass custom password hashing algorithm by setting `passwordHasher` option in the `auth` configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { scrypt } from "scrypt"
export const auth = betterAuth({
//...rest of the options
emailAndPassword: {
password: {
hash: // your custom password hashing function
verify: // your custom password verification function
}
}
})
```
# authentication: Facebook
URL: /docs/authentication/facebook
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/facebook.mdx
Facebook provider setup and usage.
***
title: Facebook
description: Facebook provider setup and usage.
-----------------------------------------------
### Get your Facebook credentials
To use Facebook sign in, you need a client ID and client Secret. You can get them from the [Facebook Developer Portal](https://developers.facebook.com/).
Select your app, navigate to **App Settings > Basic**, locate the following:
* **App ID**: This is your `clientId`
* **App Secret**: This is your `clientSecret`.
Avoid exposing the `clientSecret` in client-side code (e.g., frontend apps) because it’s sensitive information.
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/facebook` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
facebook: { // [!code highlight]
clientId: process.env.FACEBOOK_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
BetterAuth also supports Facebook Login for Business, all you need
to do is provide the `configId` as listed in **Facebook Login For Business > Configurations** alongside your `clientId` and `clientSecret`. Note that the app must be a Business app and, since BetterAuth expects to have an email address and account id, the configuration must be of the "User access token" type. "System-user access token" is not supported.
### Sign In with Facebook
To sign in with Facebook, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `facebook`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/auth-client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "facebook"
})
}
```
## Additional Configuration
### Scopes
By default, Facebook provides basic user information. If you need additional permissions, you can specify scopes in your auth configuration:
```ts title="auth.ts"
export const auth = betterAuth({
socialProviders: {
facebook: {
clientId: process.env.FACEBOOK_CLIENT_ID as string,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string,
scopes: ["email", "public_profile", "user_friends"], // Overwrites permissions
fields: ["user_friends"], // Extending list of fields
},
},
})
```
Additional options:
* `scopes`: Access basic account information (overwrites).
* Default: `"email", "public_profile"`
* `fields`: Extend list of fields to retrieve from the Facebook user profile (assignment).
* Default: `"id", "name", "email", "picture"`
### Sign In with Facebook With ID or Access Token
To sign in with Facebook using the ID Token, you can use the `signIn.social` function to pass the ID Token.
This is useful when you have the ID Token from Facebook on the client-side and want to use it to sign in on the server.
If ID token is provided no redirection will happen, and the user will be signed in directly.
For limited login, you need to pass `idToken.token`, for only `accessToken` you need to pass `idToken.accessToken` and `idToken.token` together because of (#1183)\[[https://github.com/better-auth/better-auth/issues/1183](https://github.com/better-auth/better-auth/issues/1183)].
```ts title="auth-client.ts"
const data = await authClient.signIn.social({
provider: "facebook",
idToken: { // [!code highlight]
...(platform === 'ios' ? // [!code highlight]
{ token: idToken } // [!code highlight]
: { token: accessToken, accessToken: accessToken }), // [!code highlight]
},
})
```
For a complete list of available permissions, refer to the [Permissions Reference](https://developers.facebook.com/docs/permissions).
# authentication: Figma
URL: /docs/authentication/figma
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/figma.mdx
Figma provider setup and usage.
***
title: Figma
description: Figma provider setup and usage.
--------------------------------------------
### Get your Credentials
1. Sign in to your Figma account and go to the [Developer Apps page](https://www.figma.com/developers/apps)
2. Click "Create new app"
3. Fill out the app details (name, description, etc.)
4. Configure your redirect URI (e.g., `https://yourdomain.com/api/auth/callback/figma`)
5. Note your Client ID and Client Secret
* The default scope is `file_read`. For additional scopes like `file_write`, refer to the [Figma OAuth documentation](https://www.figma.com/developers/api#oauth2).
Make sure to set the redirect URI to match your application's callback URL. If you change the base path of the auth routes, you should update the redirect URI accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
figma: { // [!code highlight]
clientId: process.env.FIGMA_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.FIGMA_CLIENT_SECRET as string, // [!code highlight]
clientKey: process.env.FIGMA_CLIENT_KEY as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Figma
To sign in with Figma, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `figma`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "figma"
})
}
```
For more information about Figma's OAuth scopes and API capabilities, refer to the [official Figma API documentation](https://www.figma.com/developers/api).
# authentication: GitHub
URL: /docs/authentication/github
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/github.mdx
GitHub provider setup and usage.
***
title: GitHub
description: GitHub provider setup and usage.
---------------------------------------------
### Get your GitHub credentials
To use GitHub sign in, you need a client ID and client secret. You can get them from the [GitHub Developer Portal](https://github.com/settings/developers).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/github` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
Important: You MUST include the user:email scope in your GitHub app. See details below.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
github: { // [!code highlight]
clientId: process.env.GITHUB_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.GITHUB_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with GitHub
To sign in with GitHub, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `github`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "github"
})
}
```
## Usage
### Setting up your Github app
Github has two types of apps: Github apps and OAuth apps.
For OAuth apps, you don't have to do anything special (just follow the steps above). For Github apps, you DO have to add one more thing, which is enable it to read the user's email:
1. After creating your app, go to *Permissions and Events* > *Account Permissions* > *Email Addresses* and select "Read-Only"
2. Save changes.
That's all! Now you can copy the Client ID and Client Secret of your app!
If you get "email\_not\_found" error, it's because you selected a Github app & did not configure this part!
### Why don't I have a refresh token?
Github doesn't issue refresh tokens for OAuth apps. For regular OAuth apps,
GitHub issues access tokens that remain valid indefinitely unless the user revokes them,
the app revokes them, or they go unused for a year.
There's no need for a refresh token because the access token doesn't expire on a short interval like Google or Discord.
# authentication: GitLab
URL: /docs/authentication/gitlab
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/gitlab.mdx
GitLab provider setup and usage.
***
title: GitLab
description: GitLab provider setup and usage.
---------------------------------------------
### Get your GitLab credentials
To use GitLab sign in, you need a client ID and client secret. [GitLab OAuth documentation](https://docs.gitlab.com/ee/api/oauth2.html).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/gitlab` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
gitlab: { // [!code highlight]
clientId: process.env.GITLAB_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.GITLAB_CLIENT_SECRET as string, // [!code highlight]
issuer: process.env.GITLAB_ISSUER as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with GitLab
To sign in with GitLab, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `gitlab`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "gitlab"
})
}
```
# authentication: Google
URL: /docs/authentication/google
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/google.mdx
Google provider setup and usage.
***
title: Google
description: Google provider setup and usage.
---------------------------------------------
### Get your Google credentials
To use Google as a social provider, you need to get your Google credentials. You can get them by creating a new project in the [Google Cloud Console](https://console.cloud.google.com/apis/dashboard).
In the Google Cloud Console > Credentials > Authorized redirect URIs, make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/google` for local development. For production, make sure to set the redirect URL as your application domain, e.g. `https://example.com/api/auth/callback/google`. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.google` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
google: { // [!code highlight]
clientId: process.env.GOOGLE_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
## Usage
### Sign In with Google
To sign in with Google, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `google`.
```ts title="auth-client.ts" /
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient();
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "google",
});
};
```
### Sign In with Google With ID Token
To sign in with Google using the ID Token, you can use the `signIn.social` function to pass the ID Token.
This is useful when you have the ID Token from Google on the client-side and want to use it to sign in on the server.
If ID token is provided no redirection will happen, and the user will be
signed in directly.
```ts title="auth-client.ts"
const data = await authClient.signIn.social({
provider: "google",
idToken: {
token: // Google ID Token,
accessToken: // Google Access Token
}
})
```
If you want to use google one tap, you can use the [One Tap
Plugin](/docs/plugins/one-tap) guide.
### Always ask to select an account
If you want to always ask the user to select an account, you pass the `prompt` parameter to the provider, setting it to `select_account`.
```ts
socialProviders: {
google: {
prompt: "select_account", // [!code highlight]
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
}
```
### Requesting Additional Google Scopes
If your application needs additional Google scopes after the user has already signed up (e.g., for Google Drive, Gmail, or other Google services), you can request them using the `linkSocial` method with the same Google provider.
```tsx title="auth-client.ts"
const requestGoogleDriveAccess = async () => {
await authClient.linkSocial({
provider: "google",
scopes: ["https://www.googleapis.com/auth/drive.file"],
});
};
// Example usage in a React component
return (
);
```
This will trigger a new OAuth flow that requests the additional scopes. After completion, your account will have the new scope in the database, and the access token will give you access to the requested Google APIs.
Ensure you're using Better Auth version 1.2.7 or later to avoid "Social
account already linked" errors when requesting additional scopes from the same
provider.
### Always get refresh token
Google only issues a refresh token the first time a user consents to your app.
If the user has already authorized your app, subsequent OAuth flows will only return an access token, not a refresh token.
To always get a refresh token, you can set the `accessType` to `offline`, and `prompt` to `select_account consent` in the provider options.
```ts
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
accessType: "offline", // [!code highlight]
prompt: "select_account consent", // [!code highlight]
},
}
```
**Revoking Access:** If you want to get a new refresh token for a user who has
already authorized your app, you must have them revoke your app's access in
their Google account settings, then re-authorize.
# authentication: Hugging Face
URL: /docs/authentication/huggingface
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/huggingface.mdx
Hugging Face provider setup and usage.
***
title: Hugging Face
description: Hugging Face provider setup and usage.
---------------------------------------------------
### Get your Hugging Face credentials
To use Hugging Face sign in, you need a client ID and client secret. [Hugging Face OAuth documentation](https://huggingface.co/docs/hub/oauth). Make sure the created oauth app on Hugging Face has the "email" scope.
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/huggingface` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
huggingface: { // [!code highlight]
clientId: process.env.HUGGINGFACE_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Hugging Face
To sign in with Hugging Face, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `huggingface`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "huggingface"
})
}
```
# authentication: Kakao
URL: /docs/authentication/kakao
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/kakao.mdx
Kakao provider setup and usage.
***
title: Kakao
description: Kakao provider setup and usage.
--------------------------------------------
### Get your Kakao Credentials
To use Kakao sign in, you need a client ID and client secret. You can get them from the [Kakao Developer Portal](https://developers.kakao.com).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/kakao` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
kakao: { // [!code highlight]
clientId: process.env.KAKAO_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.KAKAO_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
}
})
```
### Sign In with Kakao
To sign in with Kakao, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `kakao`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "kakao"
})
}
```
# authentication: Kick
URL: /docs/authentication/kick
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/kick.mdx
Kick provider setup and usage.
***
title: Kick
description: Kick provider setup and usage.
-------------------------------------------
### Get your Kick Credentials
To use Kick sign in, you need a client ID and client secret. You can get them from the [Kick Developer Portal](https://kick.com/settings/developer).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/kick` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
kick: { // [!code highlight]
clientId: process.env.KICK_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.KICK_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
}
})
```
### Sign In with Kick
To sign in with Kick, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `kick`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "kick"
})
}
```
# authentication: LINE
URL: /docs/authentication/line
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/line.mdx
LINE provider setup and usage.
***
title: LINE
description: LINE provider setup and usage.
-------------------------------------------
### Get your LINE credentials
1. Create a channel in the LINE Developers Console.
2. Note your Channel ID (client\_id) and Channel secret (client\_secret).
3. In the channel settings, add your Redirect URI, e.g. `http://localhost:3000/api/auth/callback/line` for local development.
4. Enable required scopes (at least `openid`; add `profile`, `email` if you need name, avatar, email).
See LINE Login v2.1 reference for details: \[`https://developers.line.biz/en/reference/line-login/#issue-access-token`]
### Configure the provider
Add your LINE credentials to `socialProviders.line` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
line: {
clientId: process.env.LINE_CLIENT_ID as string,
clientSecret: process.env.LINE_CLIENT_SECRET as string,
// Optional: override redirect if needed
// redirectURI: "https://your.app/api/auth/callback/line",
// scopes are prefilled: ["openid","profile","email"]. Append if needed
},
},
});
```
## Usage
### Sign In with LINE
Use the client `signIn.social` with `provider: "line"`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient();
async function signInWithLINE() {
const res = await authClient.signIn.social({ provider: "line" });
}
```
### Sign In with LINE using ID Token (optional)
If you obtain the LINE ID token on the client, you can sign in directly without redirection.
```ts title="auth-client.ts"
await authClient.signIn.social({
provider: "line",
idToken: {
token: "",
accessToken: "",
},
});
```
### Notes
* Default scopes include `openid profile email`. Adjust as needed via provider options.
* Verify redirect URI exactly matches the value configured in LINE Developers Console.
* LINE ID token verification uses the official endpoint and checks audience and optional nonce per spec.
Designing a login button? Follow LINE's button [guidelines](https://developers.line.biz/en/docs/line-login/login-button/).
# authentication: Linear
URL: /docs/authentication/linear
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/linear.mdx
Linear provider setup and usage.
***
title: Linear
description: Linear provider setup and usage.
---------------------------------------------
### Get your Linear credentials
To use Linear sign in, you need a client ID and client secret. You can get them from the [Linear Developer Portal](https://linear.app/settings/api).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/linear` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
When creating your OAuth application in Linear, you'll need to specify the required scopes. The default scope is `read`, but you can also request additional scopes like `write` if needed.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
linear: { // [!code highlight]
clientId: process.env.LINEAR_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.LINEAR_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Linear
To sign in with Linear, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `linear`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "linear"
})
}
```
### Available scopes
Linear OAuth supports the following scopes:
* `read` (default): Read access for the user's account
* `write`: Write access for the user's account
* `issues:create`: Allows creating new issues and their attachments
* `comments:create`: Allows creating new issue comments
* `timeSchedule:write`: Allows creating and modifying time schedules
* `admin`: Full access to admin level endpoints (use with caution)
You can specify additional scopes when configuring the provider:
```ts title="auth.ts"
export const auth = betterAuth({
socialProviders: {
linear: {
clientId: process.env.LINEAR_CLIENT_ID as string,
clientSecret: process.env.LINEAR_CLIENT_SECRET as string,
scope: ["read", "write"] // [!code highlight]
},
},
})
```
# authentication: LinkedIn
URL: /docs/authentication/linkedin
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/linkedin.mdx
LinkedIn Provider
***
title: LinkedIn
description: LinkedIn Provider
------------------------------
### Get your LinkedIn credentials
To use LinkedIn sign in, you need a client ID and client secret. You can get them from the [LinkedIn Developer Portal](https://www.linkedin.com/developers/).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/linkedin` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
In the LinkedIn portal under products you need the **Sign In with LinkedIn using OpenID Connect** product.
There are some different Guides here:
[Authorization Code Flow (3-legged OAuth) (Outdated)](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow)
[Sign In with LinkedIn using OpenID Connect](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?context=linkedin%2Fconsumer%2Fcontext)
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
linkedin: { // [!code highlight]
clientId: process.env.LINKEDIN_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with LinkedIn
To sign in with LinkedIn, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `linkedin`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "linkedin"
})
}
```
# authentication: Microsoft
URL: /docs/authentication/microsoft
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/microsoft.mdx
Microsoft provider setup and usage.
***
title: Microsoft
description: Microsoft provider setup and usage.
------------------------------------------------
Enabling OAuth with Microsoft Azure Entra ID (formerly Active Directory) allows your users to sign in and sign up to your application with their Microsoft account.
### Get your Microsoft credentials
To use Microsoft as a social provider, you need to get your Microsoft credentials. Which involves generating your own Client ID and Client Secret using your Microsoft Entra ID dashboard account.
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/microsoft` for local development. For production, you should change it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
see the [Microsoft Entra ID documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) for more information.
### Configure the provider
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.microsoft` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
microsoft: { // [!code highlight]
clientId: process.env.MICROSOFT_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string, // [!code highlight]
// Optional
tenantId: 'common', // [!code highlight]
authority: "https://login.microsoftonline.com", // Authentication authority URL // [!code highlight]
prompt: "select_account", // Forces account selection // [!code highlight]
}, // [!code highlight]
},
})
```
**Authority URL**: Use the default `https://login.microsoftonline.com` for standard Entra ID scenarios or `https://.ciamlogin.com` for CIAM (Customer Identity and Access Management) scenarios.
## Sign In with Microsoft
To sign in with Microsoft, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `microsoft`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient();
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "microsoft",
callbackURL: "/dashboard", // The URL to redirect to after the sign in
});
};
```
# authentication: Naver
URL: /docs/authentication/naver
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/naver.mdx
Naver provider setup and usage.
***
title: Naver
description: Naver provider setup and usage.
--------------------------------------------
### Get your Naver Credentials
To use Naver sign in, you need a client ID and client secret. You can get them from the [Naver Developers](https://developers.naver.com/).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/naver` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
naver: { // [!code highlight]
clientId: process.env.NAVER_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.NAVER_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
}
})
```
### Sign In with Naver
To sign in with Naver, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `naver`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "naver"
})
}
```
# authentication: Notion
URL: /docs/authentication/notion
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/notion.mdx
Notion provider setup and usage.
***
title: Notion
description: Notion provider setup and usage.
---------------------------------------------
### Get your Notion credentials
To use Notion as a social provider, you need to get your Notion OAuth credentials. You can get them by creating a new integration in the [Notion Developers Portal](https://www.notion.so/my-integrations).
In the Notion integration settings > OAuth Domain & URIs, make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/notion` for local development. For production, make sure to set the redirect URL as your application domain, e.g. `https://example.com/api/auth/callback/notion`. If you change the base path of the auth routes, you should update the redirect URL accordingly.
Make sure your Notion integration has the appropriate capabilities enabled. For user authentication, you'll need the "Read user information including email addresses" capability.
### Configure the provider
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.notion` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
notion: { // [!code highlight]
clientId: process.env.NOTION_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.NOTION_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
## Usage
### Sign In with Notion
To sign in with Notion, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `notion`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "notion"
})
}
```
### Notion Integration Types
Notion supports different integration types. When creating your integration, you can choose between:
* **Public integrations**: Can be installed by any Notion workspace
* **Internal integrations**: Limited to your own workspace
For most authentication use cases, you'll want to create a public integration to allow users from different workspaces to sign in.
### Requesting Additional Notion Scopes
If your application needs additional Notion capabilities after the user has already signed up, you can request them using the `linkSocial` method with the same Notion provider and additional scopes.
```ts title="auth-client.ts"
const requestNotionAccess = async () => {
await authClient.linkSocial({
provider: "notion",
// Notion automatically provides access based on integration capabilities
});
};
// Example usage in a React component
return ;
```
After authentication, you can use the access token to interact with the Notion API to read and write pages, databases, and other content that the user has granted access to.
# authentication: Other Social Providers
URL: /docs/authentication/other-social-providers
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/other-social-providers.mdx
Other social providers setup and usage.
***
title: Other Social Providers
description: Other social providers setup and usage.
----------------------------------------------------
Better Auth providers out of the box support for the [Generic Oauth Plugin](/docs/plugins/generic-oauth) which allows you to use any social provider that implements the OAuth2 protocol or OpenID Connect (OIDC) flows.
To use a provider that is not supported out of the box, you can use the [Generic Oauth Plugin](/docs/plugins/generic-oauth).
## Installation
### Add the plugin to your auth config
To use the Generic OAuth plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { genericOAuth } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
genericOAuth({ // [!code highlight]
config: [ // [!code highlight]
{ // [!code highlight]
providerId: "provider-id", // [!code highlight]
clientId: "test-client-id", // [!code highlight]
clientSecret: "test-client-secret", // [!code highlight]
discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight]
// ... other config options // [!code highlight]
}, // [!code highlight]
// Add more providers as needed // [!code highlight]
] // [!code highlight]
}) // [!code highlight]
]
})
```
### Add the client plugin
Include the Generic OAuth client plugin in your authentication client instance.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { genericOAuthClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [
genericOAuthClient()
]
})
```
Read more about installation and usage of the Generic Oauth plugin
[here](/docs/plugins/generic-oauth#usage).
## Example usage
### Instagram Example
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
export const auth = betterAuth({
// ... other config options
plugins: [
genericOAuth({
config: [
{
providerId: "instagram",
clientId: process.env.INSTAGRAM_CLIENT_ID as string,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET as string,
authorizationUrl: "https://api.instagram.com/oauth/authorize",
tokenUrl: "https://api.instagram.com/oauth/access_token",
scopes: ["user_profile", "user_media"],
},
],
}),
],
});
```
```ts title="sign-in.ts"
const response = await authClient.signIn.oauth2({
providerId: "instagram",
callbackURL: "/dashboard", // the path to redirect to after the user is authenticated
});
```
### Coinbase Example
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
export const auth = betterAuth({
// ... other config options
plugins: [
genericOAuth({
config: [
{
providerId: "coinbase",
clientId: process.env.COINBASE_CLIENT_ID as string,
clientSecret: process.env.COINBASE_CLIENT_SECRET as string,
authorizationUrl: "https://www.coinbase.com/oauth/authorize",
tokenUrl: "https://api.coinbase.com/oauth/token",
scopes: ["wallet:user:read"], // and more...
},
],
}),
],
});
```
```ts title="sign-in.ts"
const response = await authClient.signIn.oauth2({
providerId: "coinbase",
callbackURL: "/dashboard", // the path to redirect to after the user is authenticated
});
```
# authentication: PayPal
URL: /docs/authentication/paypal
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/paypal.mdx
Paypal provider setup and usage.
***
title: PayPal
description: Paypal provider setup and usage.
---------------------------------------------
### Get your PayPal Credentials
To integrate with PayPal, you need to obtain API credentials by creating an application in the [PayPal Developer Portal](https://developer.paypal.com/dashboard).
Follow these steps:
1. Create an account on the PayPal Developer Portal
2. Create a new application, [official docs](https://developer.paypal.com/developer/applications/)
3. Configure Log in with PayPal under "Other features"
4. Set up your Return URL (redirect URL)
5. Configure user information permissions
6. Note your Client ID and Client Secret
* PayPal has two environments: Sandbox (for testing) and Live (for production)
* For testing, create sandbox test accounts in the Developer Dashboard under "Sandbox" → "Accounts"
* You cannot use your real PayPal account to test in sandbox mode - you must use the generated test accounts
* The Return URL in your PayPal app settings must exactly match your redirect URI
* The PayPal API does not work with localhost. You need to use a public domain for the redirect URL and HTTPS for local testing. You can use [NGROK](https://ngrok.com/) or another similar tool for this.
Make sure to configure "Log in with PayPal" in your app settings:
1. Go to your app in the Developer Dashboard
2. Under "Other features", check "Log in with PayPal"
3. Click "Advanced Settings"
4. Enter your Return URL
5. Select the user information you want to access (email, name, etc.)
6. Enter Privacy Policy and User Agreement URLs
* PayPal doesn't use traditional OAuth2 scopes in the authorization URL. Instead, you configure permissions directly in the Developer Dashboard
* For live apps, PayPal must review and approve your application before it can go live, which typically takes a few weeks
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
paypal: { // [!code highlight]
clientId: process.env.PAYPAL_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.PAYPAL_CLIENT_SECRET as string, // [!code highlight]
environment: "sandbox", // or "live" for production //, // [!code highlight]
}, // [!code highlight]
},
})
```
#### Options
The PayPal provider accepts the following options:
* `environment`: `'sandbox' | 'live'` - PayPal environment to use (default: `'sandbox'`)
* `requestShippingAddress`: `boolean` - Whether to request shipping address information (default: `false`)
```ts title="auth.ts"
export const auth = betterAuth({
socialProviders: {
paypal: {
clientId: process.env.PAYPAL_CLIENT_ID as string,
clientSecret: process.env.PAYPAL_CLIENT_SECRET as string,
environment: "live", // Use "live" for production
requestShippingAddress: true, // Request address info
},
},
})
```
### Sign In with PayPal
To sign in with PayPal, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `paypal`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "paypal"
})
}
```
### Additional Options:
* `environment`: PayPal environment to use.
* Default: `"sandbox"`
* Options: `"sandbox"` | `"live"`
* `requestShippingAddress`: Whether to request shipping address information.
* Default: `false`
* `scope`: Additional scopes to request (combined with default permissions).
* Default: Configured in PayPal Developer Dashboard
* Note: PayPal doesn't use traditional OAuth2 scopes - permissions are set in the Dashboard
For more details refer to the [Scopes Reference](https://developer.paypal.com/docs/log-in-with-paypal/integrate/reference/#scope-attributes)
* `mapProfileToUser`: Custom function to map PayPal profile data to user object.
* `getUserInfo`: Custom function to retrieve user information.
For more details refer to the [User Reference](https://developer.paypal.com/docs/api/identity/v1/#userinfo_get)
* `verifyIdToken`: Custom ID token verification function.
# authentication: Reddit
URL: /docs/authentication/reddit
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/reddit.mdx
Reddit provider setup and usage.
***
title: Reddit
description: Reddit provider setup and usage.
---------------------------------------------
### Get your Reddit Credentials
To use Reddit sign in, you need a client ID and client secret. You can get them from the [Reddit Developer Portal](https://www.reddit.com/prefs/apps).
1. Click "Create App" or "Create Another App"
2. Select "web app" as the application type
3. Set the redirect URL to `http://localhost:3000/api/auth/callback/reddit` for local development
4. For production, set it to your application's domain (e.g. `https://example.com/api/auth/callback/reddit`)
5. After creating the app, you'll get the client ID (under the app name) and client secret
If you change the base path of the auth routes, make sure to update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
reddit: {
clientId: process.env.REDDIT_CLIENT_ID as string,
clientSecret: process.env.REDDIT_CLIENT_SECRET as string,
},
},
})
```
### Sign In with Reddit
To sign in with Reddit, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `reddit`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "reddit"
})
}
```
## Additional Configuration
### Scopes
By default, Reddit provides basic user information. If you need additional permissions, you can specify scopes in your auth configuration:
```ts title="auth.ts"
export const auth = betterAuth({
socialProviders: {
reddit: {
clientId: process.env.REDDIT_CLIENT_ID as string,
clientSecret: process.env.REDDIT_CLIENT_SECRET as string,
duration: "permanent",
scope: ["read", "submit"] // Add required scopes
},
},
})
```
Common Reddit scopes include:
* `identity`: Access basic account information
* `read`: Access posts and comments
* `submit`: Submit posts and comments
* `subscribe`: Manage subreddit subscriptions
* `history`: Access voting history
For a complete list of available scopes, refer to the [Reddit OAuth2 documentation](https://www.reddit.com/dev/api/oauth).
# authentication: Roblox
URL: /docs/authentication/roblox
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/roblox.mdx
Roblox provider setup and usage.
***
title: Roblox
description: Roblox provider setup and usage.
---------------------------------------------
### Get your Roblox Credentials
Get your Roblox credentials from the [Roblox Creator Hub](https://create.roblox.com/dashboard/credentials?activeTab=OAuthTab).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/roblox` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
The Roblox API does not provide email addresses. As a workaround, the user's `email` field uses the `preferred_username` value instead.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
roblox: { // [!code highlight]
clientId: process.env.ROBLOX_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.ROBLOX_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Roblox
To sign in with Roblox, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `roblox`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "roblox"
})
}
```
# authentication: Salesforce
URL: /docs/authentication/salesforce
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/salesforce.mdx
Salesforce provider setup and usage.
***
title: Salesforce
description: Salesforce provider setup and usage.
-------------------------------------------------
### Get your Salesforce Credentials
1. Log into your Salesforce org (Production or Developer Edition)
2. Navigate to **Setup > App Manager**
3. Click **New Connected App**
4. Fill in the basic information:
* Connected App Name: Your app name
* API Name: Auto-generated from app name
* Contact Email: Your email address
5. Enable OAuth Settings:
* Check **Enable OAuth Settings**
* Set **Callback URL** to your redirect URI (e.g., `http://localhost:3000/api/auth/callback/salesforce` for development)
* Select Required OAuth Scopes:
* Access your basic information (id)
* Access your identity URL service (openid)
* Access your email address (email)
* Perform requests on your behalf at any time (refresh\_token, offline\_access)
6. Enable **Require Proof Key for Code Exchange (PKCE)** (required)
7. Save and note your **Consumer Key** (Client ID) and **Consumer Secret** (Client Secret)
* For development, you can use `http://localhost:3000` URLs, but production requires HTTPS
* The callback URL must exactly match what's configured in Better Auth
* PKCE (Proof Key for Code Exchange) is required by Salesforce and is automatically handled by the provider
For sandbox testing, you can create the Connected App in your sandbox org, or use the same Connected App but specify `environment: "sandbox"` in the provider configuration.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
salesforce: { // [!code highlight]
clientId: process.env.SALESFORCE_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string, // [!code highlight]
environment: "production", // or "sandbox" // [!code highlight]
}, // [!code highlight]
},
})
```
#### Configuration Options
* `clientId`: Your Connected App's Consumer Key
* `clientSecret`: Your Connected App's Consumer Secret
* `environment`: `"production"` (default) or `"sandbox"`
* `loginUrl`: Custom My Domain URL (without `https://`) - overrides environment setting
* `redirectURI`: Override the auto-generated redirect URI if needed
#### Advanced Configuration
```ts title="auth.ts"
export const auth = betterAuth({
socialProviders: {
salesforce: {
clientId: process.env.SALESFORCE_CLIENT_ID as string,
clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string,
environment: "sandbox", // [!code highlight]
loginUrl: "mycompany.my.salesforce.com", // Custom My Domain // [!code highlight]
redirectURI: "http://localhost:3000/api/auth/callback/salesforce", // Override if needed // [!code highlight]
},
},
})
```
* Use `environment: "sandbox"` for testing with Salesforce sandbox orgs
* The `loginUrl` option is useful for organizations with My Domain enabled
* The `redirectURI` option helps resolve redirect URI mismatch errors
### Environment Variables
Add the following environment variables to your `.env.local` file:
```bash title=".env.local"
SALESFORCE_CLIENT_ID=your_consumer_key_here
SALESFORCE_CLIENT_SECRET=your_consumer_secret_here
BETTER_AUTH_URL=http://localhost:3000 # Important for redirect URI generation
```
For production:
```bash title=".env"
SALESFORCE_CLIENT_ID=your_consumer_key_here
SALESFORCE_CLIENT_SECRET=your_consumer_secret_here
BETTER_AUTH_URL=https://yourdomain.com
```
### Sign In with Salesforce
To sign in with Salesforce, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `salesforce`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "salesforce"
})
}
```
### Troubleshooting
#### Redirect URI Mismatch Error
If you encounter a `redirect_uri_mismatch` error:
1. **Check Callback URL**: Ensure the Callback URL in your Salesforce Connected App exactly matches your Better Auth callback URL
2. **Protocol**: Make sure you're using the same protocol (`http://` vs `https://`)
3. **Port**: Verify the port number matches (e.g., `:3000`)
4. **Override if needed**: Use the `redirectURI` option to explicitly set the redirect URI
```ts
salesforce: {
clientId: process.env.SALESFORCE_CLIENT_ID as string,
clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string,
redirectURI: "http://localhost:3000/api/auth/callback/salesforce", // [!code highlight]
}
```
#### Environment Issues
* **Production**: Use `environment: "production"` (default) with `login.salesforce.com`
* **Sandbox**: Use `environment: "sandbox"` with `test.salesforce.com`
* **My Domain**: Use `loginUrl: "yourcompany.my.salesforce.com"` for custom domains
#### PKCE Requirements
Salesforce requires PKCE (Proof Key for Code Exchange) which is automatically handled by this provider. Make sure PKCE is enabled in your Connected App settings.
The default scopes requested are `openid`, `email`, and `profile`. The provider will automatically include the `id` scope for accessing basic user information.
# authentication: Slack
URL: /docs/authentication/slack
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/slack.mdx
Slack provider setup and usage.
***
title: Slack
description: Slack provider setup and usage.
--------------------------------------------
### Get your Slack credentials
To use Slack as a social provider, you need to create a Slack app and get your credentials.
1. Go to [Your Apps on Slack API](https://api.slack.com/apps) and click "Create New App"
2. Choose "From scratch" and give your app a name and select a development workspace
3. In your app settings, navigate to "OAuth & Permissions"
4. Under "Redirect URLs", add your redirect URL:
* For local development: `http://localhost:3000/api/auth/callback/slack`
* For production: `https://yourdomain.com/api/auth/callback/slack`
5. Copy your Client ID and Client Secret from the "Basic Information" page
Slack requires HTTPS for redirect URLs in production. For local development, you can use tools like [ngrok](https://ngrok.com/) to create a secure tunnel.
### Configure the provider
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.slack` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
slack: { // [!code highlight]
clientId: process.env.SLACK_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.SLACK_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
## Usage
### Sign In with Slack
To sign in with Slack, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `slack`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient();
const signIn = async () => {
const data = await authClient.signIn.social({ provider: "slack" });
};
```
### Requesting Additional Scopes
By default, Slack uses OpenID Connect scopes: `openid`, `profile`, and `email`. You can request additional Slack scopes during sign-in:
```ts title="auth-client.ts"
const signInWithSlack = async () => {
await authClient.signIn.social({
provider: "slack",
scopes: ["channels:read", "chat:write"], // Additional Slack API scopes
});
};
```
### Workspace-Specific Sign In
If you want to restrict sign-in to a specific Slack workspace, you can pass the `team` parameter:
```ts title="auth.ts"
socialProviders: {
slack: {
clientId: process.env.SLACK_CLIENT_ID as string,
clientSecret: process.env.SLACK_CLIENT_SECRET as string,
team: "T1234567890", // Your Slack workspace ID
},
}
```
### Using Slack API After Sign In
After successful authentication, you can access the user's Slack information through the session. The access token can be used to make requests to the Slack API:
```ts
const session = await authClient.getSession();
if (session?.user) {
// Access Slack-specific data
const slackUserId = session.user.id; // This is the Slack user ID
// The access token is stored securely on the server
}
```
The Slack provider uses OpenID Connect by default, which provides basic user
information. If you need to access other Slack APIs, make sure to request the
appropriate scopes during sign-in.
# authentication: Spotify
URL: /docs/authentication/spotify
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/spotify.mdx
Spotify provider setup and usage.
***
title: Spotify
description: Spotify provider setup and usage.
----------------------------------------------
### Get your Spotify Credentials
To use Spotify sign in, you need a client ID and client secret. You can get them from the [Spotify Developer Portal](https://developer.spotify.com/dashboard/applications).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/spotify` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
spotify: { // [!code highlight]
clientId: process.env.SPOTIFY_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Spotify
To sign in with Spotify, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `spotify`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "spotify"
})
}
```
# authentication: TikTok
URL: /docs/authentication/tiktok
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/tiktok.mdx
TikTok provider setup and usage.
***
title: TikTok
description: TikTok provider setup and usage.
---------------------------------------------
### Get your TikTok Credentials
To integrate with TikTok, you need to obtain API credentials by creating an application in the [TikTok Developer Portal](https://developers.tiktok.com/apps).
Follow these steps:
1. Create an account on the TikTok Developer Portal
2. Create a new application
3. Set up a sandbox environment for testing
4. Configure your redirect URL (must be HTTPS)
5. Note your Client Secret and Client Key
* The TikTok API does not work with localhost. You need to use a public domain for the redirect URL and HTTPS for local testing. You can use [NGROK](https://ngrok.com/) or another similar tool for this.
* For testing, you will need to use the [Sandbox mode](https://developers.tiktok.com/blog/introducing-sandbox), which you can enable in the TikTok Developer Portal.
* The default scope is `user.info.profile`. For additional scopes, refer to the [Available Scopes](https://developers.tiktok.com/doc/tiktok-api-scopes/) documentation.
Make sure to set the redirect URL to a valid HTTPS domain for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
* The TikTok API does not provide email addresses. As a workaround, this implementation uses the user's `username` value for the `email` field, which is why it requires the `user.info.profile` scope instead of just `user.info.basic`.
* For production use, you will need to request approval from TikTok for the scopes you intend to use.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
tiktok: { // [!code highlight]
clientSecret: process.env.TIKTOK_CLIENT_SECRET as string, // [!code highlight]
clientKey: process.env.TIKTOK_CLIENT_KEY as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with TikTok
To sign in with TikTok, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `tiktok`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "tiktok"
})
}
```
# authentication: Twitch
URL: /docs/authentication/twitch
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/twitch.mdx
Twitch provider setup and usage.
***
title: Twitch
description: Twitch provider setup and usage.
---------------------------------------------
### Get your Twitch Credentials
To use Twitch sign in, you need a client ID and client secret. You can get them from the [Twitch Developer Portal](https://dev.twitch.tv/console/apps).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/twitch` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
twitch: { // [!code highlight]
clientId: process.env.TWITCH_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.TWITCH_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
}
})
```
### Sign In with Twitch
To sign in with Twitch, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `twitch`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "twitch"
})
}
```
# authentication: Twitter (X)
URL: /docs/authentication/twitter
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/twitter.mdx
Twitter provider setup and usage.
***
title: Twitter (X)
description: Twitter provider setup and usage.
----------------------------------------------
### Get your Twitter Credentials
Get your Twitter credentials from the [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/twitter` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
Twitter API v2 now supports email address retrieval. Make sure to request the `user.email` scope when configuring your Twitter app to enable this feature.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
twitter: { // [!code highlight]
clientId: process.env.TWITTER_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.TWITTER_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Twitter
To sign in with Twitter, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `twitter`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "twitter"
})
}
```
# authentication: VK
URL: /docs/authentication/vk
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/vk.mdx
VK ID Provider
***
title: VK
description: VK ID Provider
---------------------------
### Get your VK ID credentials
To use VK ID sign in, you need a client ID and client secret. You can get them from the [VK ID Developer Portal](https://id.vk.com/about/business/go/docs).
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/vk` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
vk: { // [!code highlight]
clientId: process.env.VK_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.VK_CLIENT_SECRET as string, // [!code highlight]
},
},
});
```
### Sign In with VK
To sign in with VK, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
* `provider`: The provider to use. It should be set to `vk`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient();
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "vk",
});
};
```
# authentication: Zoom
URL: /docs/authentication/zoom
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/authentication/zoom.mdx
Zoom provider setup and usage.
***
title: Zoom
description: Zoom provider setup and usage.
-------------------------------------------
### Create a Zoom App from Marketplace
1. Visit [Zoom Marketplace](https://marketplace.zoom.us).
2. Hover on the `Develop` button and select `Build App`
3. Select `General App` and click `Create`
### Configure your Zoom App
Ensure that you are in the `Basic Information` of your app settings.
1. Under `Select how the app is managed`, choose `User-managed`
2. Under `App Credentials`, copy your `Client ID` and `Client Secret` and store them in a safe location
3. Under `OAuth Information` -> `OAuth Redirect URL`, add your Callback URL. For example,
```
http://localhost:3000/api/auth/callback/zoom
```
For production, you should set it to the URL of your application. If you change the base
path of the auth routes, you should update the redirect URL accordingly.
Skip to the `Scopes` section, then
1. Click the `Add Scopes` button
2. Search for `user:read:user` (View a user) and select it
3. Add any other scopes your applications needs and click `Done`
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
zoom: { // [!code highlight]
clientId: process.env.ZOOM_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.ZOOM_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
### Sign In with Zoom
To sign in with Zoom, you can use the `signIn.social` function provided by the client.
You will need to specify `zoom` as the provider.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "zoom"
})
}
```
# concepts: API
URL: /docs/concepts/api
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/api.mdx
Better Auth API.
***
title: API
description: Better Auth API.
-----------------------------
When you create a new Better Auth instance, it provides you with an `api` object. This object exposes every endpoint that exists in your Better Auth instance. And you can use this to interact with Better Auth server side.
Any endpoint added to Better Auth, whether from plugins or the core, will be accessible through the `api` object.
## Calling API Endpoints on the Server
To call an API endpoint on the server, import your `auth` instance and call the endpoint using the `api` object.
```ts title="server.ts"
import { betterAuth } from "better-auth";
import { headers } from "next/headers";
export const auth = betterAuth({
//...
})
// calling get session on the server
await auth.api.getSession({
headers: await headers() // some endpoints might require headers
})
```
### Body, Headers, Query
Unlike the client, the server needs the values to be passed as an object with the key `body` for the body, `headers` for the headers, and `query` for query parameters.
```ts title="server.ts"
await auth.api.getSession({
headers: await headers()
})
await auth.api.signInEmail({
body: {
email: "john@doe.com",
password: "password"
},
headers: await headers() // optional but would be useful to get the user IP, user agent, etc.
})
await auth.api.verifyEmail({
query: {
token: "my_token"
}
})
```
Better Auth API endpoints are built on top of [better-call](https://github.com/bekacru/better-call), a tiny web framework that lets you call REST API endpoints as if they were regular functions and allows us to easily infer client types from the server.
### Getting `headers` and `Response` Object
When you invoke an API endpoint on the server, it will return a standard JavaScript object or array directly as it's just a regular function call.
But there are times when you might want to get the `headers` or the `Response` object instead. For example, if you need to get the cookies or the headers.
#### Getting `headers`
To get the `headers`, you can pass the `returnHeaders` option to the endpoint.
```ts
const { headers, response } = await auth.api.signUpEmail({
returnHeaders: true,
body: {
email: "john@doe.com",
password: "password",
name: "John Doe",
},
});
```
The `headers` will be a `Headers` object, which you can use to get the cookies or the headers.
```ts
const cookies = headers.get("set-cookie");
const headers = headers.get("x-custom-header");
```
#### Getting `Response` Object
To get the `Response` object, you can pass the `asResponse` option to the endpoint.
```ts title="server.ts"
const response = await auth.api.signInEmail({
body: {
email: "",
password: ""
},
asResponse: true
})
```
### Error Handling
When you call an API endpoint on the server, it will throw an error if the request fails. You can catch the error and handle it as you see fit. The error instance is an instance of `APIError`.
```ts title="server.ts"
import { APIError } from "better-auth/api";
try {
await auth.api.signInEmail({
body: {
email: "",
password: ""
}
})
} catch (error) {
if (error instanceof APIError) {
console.log(error.message, error.status)
}
}
```
# concepts: CLI
URL: /docs/concepts/cli
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/cli.mdx
Built-in CLI for managing your project.
***
title: CLI
description: Built-in CLI for managing your project.
----------------------------------------------------
Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, generate a secret key for your application, and gather diagnostic information about your setup.
## Generate
The `generate` command creates the schema required by Better Auth. If you're using a database adapter like Prisma or Drizzle, this command will generate the right schema for your ORM. If you're using the built-in Kysely adapter, it will generate an SQL file you can run directly on your database.
```bash title="Terminal"
npx @better-auth/cli@latest generate
```
### Options
* `--output` - Where to save the generated schema. For Prisma, it will be saved in prisma/schema.prisma. For Drizzle, it goes to schema.ts in your project root. For Kysely, it's an SQL file saved as schema.sql in your project root.
* `--config` - The path to your Better Auth config file. By default, the CLI will search for an auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under the `src` directory.
* `--yes` - Skip the confirmation prompt and generate the schema directly.
## Migrate
The migrate command applies the Better Auth schema directly to your database. This is available if you're using the built-in Kysely adapter. For other adapters, you'll need to apply the schema using your ORM's migration tool.
```bash title="Terminal"
npx @better-auth/cli@latest migrate
```
### Options
* `--config` - The path to your Better Auth config file. By default, the CLI will search for an auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under the `src` directory.
* `--yes` - Skip the confirmation prompt and apply the schema directly.
## Init
The `init` command allows you to initialize Better Auth in your project.
```bash title="Terminal"
npx @better-auth/cli@latest init
```
### Options
* `--name` - The name of your application. (defaults to the `name` property in your `package.json`).
* `--framework` - The framework your codebase is using. Currently, the only supported framework is `Next.js`.
* `--plugins` - The plugins you want to use. You can specify multiple plugins by separating them with a comma.
* `--database` - The database you want to use. Currently, the only supported database is `SQLite`.
* `--package-manager` - The package manager you want to use. Currently, the only supported package managers are `npm`, `pnpm`, `yarn`, `bun` (defaults to the manager you used to initialize the CLI).
## Info
The `info` command provides diagnostic information about your Better Auth setup and environment. Useful for debugging and sharing when seeking support.
```bash title="Terminal"
npx @better-auth/cli@latest info
```
### Output
The command displays:
* **System**: OS, CPU, memory, Node.js version
* **Package Manager**: Detected manager and version
* **Better Auth**: Version and configuration (sensitive data auto-redacted)
* **Frameworks**: Detected frameworks (Next.js, React, Vue, etc.)
* **Databases**: Database clients and ORMs (Prisma, Drizzle, etc.)
### Options
* `--config` - Path to your Better Auth config file
* `--json` - Output as JSON for sharing or programmatic use
### Examples
```bash
# Basic usage
npx @better-auth/cli@latest info
# Custom config path
npx @better-auth/cli@latest info --config ./config/auth.ts
# JSON output
npx @better-auth/cli@latest info --json > auth-info.json
```
Sensitive data like secrets, API keys, and database URLs are automatically replaced with `[REDACTED]` for safe sharing.
## Secret
The CLI also provides a way to generate a secret key for your Better Auth instance.
```bash title="Terminal"
npx @better-auth/cli@latest secret
```
## Common Issues
**Error: Cannot find module X**
If you see this error, it means the CLI can't resolve imported modules in your Better Auth config file. We are working on a fix for many of these issues, but in the meantime, you can try the following:
* Remove any import aliases in your config file and use relative paths instead. After running the CLI, you can revert to using aliases.
# concepts: Client
URL: /docs/concepts/client
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/client.mdx
Better Auth client library for authentication.
***
title: Client
description: Better Auth client library for authentication.
-----------------------------------------------------------
Better Auth offers a client library compatible with popular frontend frameworks like React, Vue, Svelte, and more. This client library includes a set of functions for interacting with the Better Auth server. Each framework's client library is built on top of a core client library that is framework-agnostic, so that all methods and hooks are consistently available across all client libraries.
## Installation
If you haven't already, install better-auth.
npm
pnpm
yarn
bun
```bash
npm i better-auth
```
```bash
pnpm add better-auth
```
```bash
yarn add better-auth
```
```bash
bun add better-auth
```
## Create Client Instance
Import `createAuthClient` from the package for your framework (e.g., "better-auth/react" for React). Call the function to create your client. Pass the base URL of your auth server. If the auth server is running on the same domain as your client, you can skip this step.
If you're using a different base path other than `/api/auth`, make sure to pass the whole URL, including the path. (e.g., `http://localhost:3000/custom-path/auth`)
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/client"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/vue"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/svelte"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/solid"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
})
```
## Usage
Once you've created your client instance, you can use the client to interact with the Better Auth server. The client provides a set of functions by default and they can be extended with plugins.
**Example: Sign In**
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
await authClient.signIn.email({
email: "test@user.com",
password: "password1234"
})
```
### Hooks
In addition to the standard methods, the client provides hooks to easily access different reactive data. Every hook is available in the root object of the client and they all start with `use`.
**Example: useSession**
```tsx title="user.tsx"
//make sure you're using the react client
import { createAuthClient } from "better-auth/react"
const { useSession } = createAuthClient() // [!code highlight]
export function User() {
const {
data: session,
isPending, //loading state
error, //error object
refetch //refetch the session
} = useSession()
return (
//...
)
}
```
```vue title="user.vue"
{{ session.data }}
```
```svelte title="user.svelte"
{#if $session}
{$session?.data?.user.name}
{$session?.data?.user.email}
{:else}
{/if}
```
```tsx title="user.tsx"
import { client } from "~/lib/client";
import { Show } from 'solid-js';
export default function Home() {
const session = client.useSession()
return (
Log in}
>
);
}
```
### Fetch Options
The client uses a library called [better fetch](https://better-fetch.vercel.app) to make requests to the server.
Better fetch is a wrapper around the native fetch API that provides a more convenient way to make requests. It's created by the same team behind Better Auth and is designed to work seamlessly with it.
You can pass any default fetch options to the client by passing `fetchOptions` object to the `createAuthClient`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient({
fetchOptions: {
//any better-fetch options
},
})
```
You can also pass fetch options to most of the client functions. Either as the second argument or as a property in the object.
```ts title="auth-client.ts"
await authClient.signIn.email({
email: "email@email.com",
password: "password1234",
}, {
onSuccess(ctx) {
//
}
})
//or
await authClient.signIn.email({
email: "email@email.com",
password: "password1234",
fetchOptions: {
onSuccess(ctx) {
//
}
},
})
```
### Handling Errors
Most of the client functions return a response object with the following properties:
* `data`: The response data.
* `error`: The error object if there was an error.
The error object contains the following properties:
* `message`: The error message. (e.g., "Invalid email or password")
* `status`: The HTTP status code.
* `statusText`: The HTTP status text.
```ts title="auth-client.ts"
const { data, error } = await authClient.signIn.email({
email: "email@email.com",
password: "password1234"
})
if (error) {
//handle error
}
```
If the action accepts a `fetchOptions` option, you can pass an `onError` callback to handle errors.
```ts title="auth-client.ts"
await authClient.signIn.email({
email: "email@email.com",
password: "password1234",
}, {
onError(ctx) {
//handle error
}
})
//or
await authClient.signIn.email({
email: "email@email.com",
password: "password1234",
fetchOptions: {
onError(ctx) {
//handle error
}
}
})
```
Hooks like `useSession` also return an error object if there was an error fetching the session. On top of that, they also return an `isPending` property to indicate if the request is still pending.
```ts title="auth-client.ts"
const { data, error, isPending } = useSession()
if (error) {
//handle error
}
```
#### Error Codes
The client instance contains $ERROR\_CODES object that contains all the error codes returned by the server. You can use this to handle error translations or custom error messages.
```ts title="auth-client.ts"
const authClient = createAuthClient();
type ErrorTypes = Partial<
Record<
keyof typeof authClient.$ERROR_CODES,
{
en: string;
es: string;
}
>
>;
const errorCodes = {
USER_ALREADY_EXISTS: {
en: "user already registered",
es: "usuario ya registrada",
},
} satisfies ErrorTypes;
const getErrorMessage = (code: string, lang: "en" | "es") => {
if (code in errorCodes) {
return errorCodes[code as keyof typeof errorCodes][lang];
}
return "";
};
const { error } = await authClient.signUp.email({
email: "user@email.com",
password: "password",
name: "User",
});
if(error?.code){
alert(getErrorMessage(error.code, "en"));
}
```
### Plugins
You can extend the client with plugins to add more functionality. Plugins can add new functions to the client or modify existing ones.
**Example: Magic Link Plugin**
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { magicLinkClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [
magicLinkClient()
]
})
```
once you've added the plugin, you can use the new functions provided by the plugin.
```ts title="auth-client.ts"
await authClient.signIn.magicLink({
email: "test@email.com"
})
```
# concepts: Cookies
URL: /docs/concepts/cookies
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/cookies.mdx
Learn how cookies are used in Better Auth.
***
title: Cookies
description: Learn how cookies are used in Better Auth.
-------------------------------------------------------
Cookies are used to store data such as session tokens, OAuth state, and more. All cookies are signed using the `secret` key provided in the auth options.
### Cookie Prefix
By default, Better Auth cookies follow the format `${prefix}.${cookie_name}`. The default prefix is "better-auth". You can change the prefix by setting `cookiePrefix` in the `advanced` object of the auth options.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
advanced: {
cookiePrefix: "my-app"
}
})
```
### Custom Cookies
All cookies are `httpOnly` and `secure` when the server is running in production mode.
If you want to set custom cookie names and attributes, you can do so by setting `cookieOptions` in the `advanced` object of the auth options.
By default, Better Auth uses the following cookies:
* `session_token` to store the session token
* `session_data` to store the session data if cookie cache is enabled
* `dont_remember` to store the flag when `rememberMe` is disabled
Plugins may also use cookies to store data. For example, the Two Factor Authentication plugin uses the `two_factor` cookie to store the two-factor authentication state.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
advanced: {
cookies: {
session_token: {
name: "custom_session_token",
attributes: {
// Set custom cookie attributes
}
},
}
}
})
```
### Cross Subdomain Cookies
Sometimes you may need to share cookies across subdomains.
For example, if you authenticate on `auth.example.com`, you may also want to access the same session on `app.example.com`.
The `domain` attribute controls which domains can access the cookie. Setting it to your root domain (e.g. `example.com`) makes the cookie accessible across all subdomains. For security, follow these guidelines:
1. Only enable cross-subdomain cookies if it's necessary
2. Set the domain to the most specific scope needed (e.g. `app.example.com` instead of `.example.com`)
3. Be cautious of untrusted subdomains that could potentially access these cookies
4. Consider using separate domains for untrusted services (e.g. `status.company.com` vs `app.company.com`)
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
advanced: {
crossSubDomainCookies: {
enabled: true,
domain: "app.example.com", // your domain
},
},
trustedOrigins: [
'https://example.com',
'https://app1.example.com',
'https://app2.example.com',
],
})
```
### Secure Cookies
By default, cookies are secure only when the server is running in production mode. You can force cookies to be always secure by setting `useSecureCookies` to `true` in the `advanced` object in the auth options.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
advanced: {
useSecureCookies: true
}
})
```
# concepts: Database
URL: /docs/concepts/database
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/database.mdx
Learn how to use a database with Better Auth.
***
title: Database
description: Learn how to use a database with Better Auth.
----------------------------------------------------------
## Adapters
Better Auth requires a database connection to store data. The database will be used to store data such as users, sessions, and more. Plugins can also define their own database tables to store data.
You can pass a database connection to Better Auth by passing a supported database instance in the database options. You can learn more about supported database adapters in the [Other relational databases](/docs/adapters/other-relational-databases) documentation.
## CLI
Better Auth comes with a CLI tool to manage database migrations and generate schema.
### Running Migrations
The cli checks your database and prompts you to add missing tables or update existing ones with new columns. This is only supported for the built-in Kysely adapter. For other adapters, you can use the `generate` command to create the schema and handle the migration through your ORM.
```bash
npx @better-auth/cli migrate
```
### Generating Schema
Better Auth also provides a `generate` command to generate the schema required by Better Auth. The `generate` command creates the schema required by Better Auth. If you're using a database adapter like Prisma or Drizzle, this command will generate the right schema for your ORM. If you're using the built-in Kysely adapter, it will generate an SQL file you can run directly on your database.
```bash
npx @better-auth/cli generate
```
See the [CLI](/docs/concepts/cli) documentation for more information on the CLI.
If you prefer adding tables manually, you can do that as well. The core schema
required by Better Auth is described below and you can find additional schema
required by plugins in the plugin documentation.
## Secondary Storage
Secondary storage in Better Auth allows you to use key-value stores for managing session data, rate limiting counters, etc. This can be useful when you want to offload the storage of this intensive records to a high performance storage or even RAM.
### Implementation
To use secondary storage, implement the `SecondaryStorage` interface:
```typescript
interface SecondaryStorage {
get: (key: string) => Promise;
set: (key: string, value: string, ttl?: number) => Promise;
delete: (key: string) => Promise;
}
```
Then, provide your implementation to the `betterAuth` function:
```typescript
betterAuth({
// ... other options
secondaryStorage: {
// Your implementation here
},
});
```
**Example: Redis Implementation**
Here's a basic example using Redis:
```typescript
import { createClient } from "redis";
import { betterAuth } from "better-auth";
const redis = createClient();
await redis.connect();
export const auth = betterAuth({
// ... other options
secondaryStorage: {
get: async (key) => {
return await redis.get(key);
},
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, { EX: ttl });
// or for ioredis:
// if (ttl) await redis.set(key, value, 'EX', ttl)
else await redis.set(key, value);
},
delete: async (key) => {
await redis.del(key);
}
}
});
```
This implementation allows Better Auth to use Redis for storing session data and rate limiting counters. You can also add prefixes to the keys names.
## Core Schema
Better Auth requires the following tables to be present in the database. The types are in `typescript` format. You can use corresponding types in your database.
### User
Table Name: `user`
### Session
Table Name: `session`
### Account
Table Name: `account`
### Verification
Table Name: `verification`
## Custom Tables
Better Auth allows you to customize the table names and column names for the core schema. You can also extend the core schema by adding additional fields to the user and session tables.
### Custom Table Names
You can customize the table names and column names for the core schema by using the `modelName` and `fields` properties in your auth config:
```ts title="auth.ts"
export const auth = betterAuth({
user: {
modelName: "users",
fields: {
name: "full_name",
email: "email_address",
},
},
session: {
modelName: "user_sessions",
fields: {
userId: "user_id",
},
},
});
```
Type inference in your code will still use the original field names (e.g.,
`user.name`, not `user.full_name`).
To customize table names and column name for plugins, you can use the `schema` property in the plugin config:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
twoFactor({
schema: {
user: {
fields: {
twoFactorEnabled: "two_factor_enabled",
secret: "two_factor_secret",
},
},
},
}),
],
});
```
### Extending Core Schema
Better Auth provides a type-safe way to extend the `user` and `session` schemas. You can add custom fields to your auth config, and the CLI will automatically update the database schema. These additional fields will be properly inferred in functions like `useSession`, `signUp.email`, and other endpoints that work with user or session objects.
To add custom fields, use the `additionalFields` property in the `user` or `session` object of your auth config. The `additionalFields` object uses field names as keys, with each value being a `FieldAttributes` object containing:
* `type`: The data type of the field (e.g., "string", "number", "boolean").
* `required`: A boolean indicating if the field is mandatory.
* `defaultValue`: The default value for the field (note: this only applies in the JavaScript layer; in the database, the field will be optional).
* `input`: This determines whether a value can be provided when creating a new record (default: `true`). If there are additional fields, like `role`, that should not be provided by the user during signup, you can set this to `false`.
Here's an example of how to extend the user schema with additional fields:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
user: {
additionalFields: {
role: {
type: "string",
required: false,
defaultValue: "user",
input: false, // don't allow user to set role
},
lang: {
type: "string",
required: false,
defaultValue: "en",
},
},
},
});
```
Now you can access the additional fields in your application logic.
```ts
//on signup
const res = await auth.api.signUpEmail({
email: "test@example.com",
password: "password",
name: "John Doe",
lang: "fr",
});
//user object
res.user.role; // > "admin"
res.user.lang; // > "fr"
```
See the
[TypeScript](/docs/concepts/typescript#inferring-additional-fields-on-client)
documentation for more information on how to infer additional fields on the
client side.
If you're using social / OAuth providers, you may want to provide `mapProfileToUser` to map the profile data to the user object. So, you can populate additional fields from the provider's profile.
**Example: Mapping Profile to User For `firstName` and `lastName`**
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
github: {
clientId: "YOUR_GITHUB_CLIENT_ID",
clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
mapProfileToUser: (profile) => {
return {
firstName: profile.name.split(" ")[0],
lastName: profile.name.split(" ")[1],
};
},
},
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
mapProfileToUser: (profile) => {
return {
firstName: profile.given_name,
lastName: profile.family_name,
};
},
},
},
});
```
### ID Generation
Better Auth by default will generate unique IDs for users, sessions, and other entities. If you want to customize how IDs are generated, you can configure this in the `advanced.database.generateId` option in your auth config.
You can also disable ID generation by setting the `advanced.database.generateId` option to `false`. This will assume your database will generate the ID automatically.
**Example: Automatic Database IDs**
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { db } from "./db";
export const auth = betterAuth({
database: {
db: db,
},
advanced: {
database: {
generateId: false,
},
},
});
```
### Database Hooks
Database hooks allow you to define custom logic that can be executed during the lifecycle of core database operations in Better Auth. You can create hooks for the following models: **user**, **session**, and **account**.
There are two types of hooks you can define:
#### 1. Before Hook
* **Purpose**: This hook is called before the respective entity (user, session, or account) is created or updated.
* **Behavior**: If the hook returns `false`, the operation will be aborted. And If it returns a data object, it'll replace the original payload.
#### 2. After Hook
* **Purpose**: This hook is called after the respective entity is created or updated.
* **Behavior**: You can perform additional actions or modifications after the entity has been successfully created or updated.
**Example Usage**
```typescript title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
// Modify the user object before it is created
return {
data: {
...user,
firstName: user.name.split(" ")[0],
lastName: user.name.split(" ")[1],
},
};
},
after: async (user) => {
//perform additional actions, like creating a stripe customer
},
},
},
},
});
```
#### Throwing Errors
If you want to stop the database hook from proceeding, you can throw errors using the `APIError` class imported from `better-auth/api`.
```typescript title="auth.ts"
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
export const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
if (user.isAgreedToTerms === false) {
// Your special condition.
// Send the API error.
throw new APIError("BAD_REQUEST", {
message: "User must agree to the TOS before signing up.",
});
}
return {
data: user,
};
},
},
},
},
});
```
#### Using the Context Object
The context object (`ctx`), passed as the second argument to the hook, contains useful information. For `update` hooks, this includes the current `session`, which you can use to access the logged-in user's details.
```typescript title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
databaseHooks: {
user: {
update: {
before: async (data, ctx) => {
// You can access the session from the context object.
if (ctx.context.session) {
console.log("User update initiated by:", ctx.context.session.userId);
}
return { data };
},
},
},
},
});
```
Much like standard hooks, database hooks also provide a `ctx` object that offers a variety of useful properties. Learn more in the [Hooks Documentation](/docs/concepts/hooks#ctx).
## Plugins Schema
Plugins can define their own tables in the database to store additional data. They can also add columns to the core tables to store additional data. For example, the two factor authentication plugin adds the following columns to the `user` table:
* `twoFactorEnabled`: Whether two factor authentication is enabled for the user.
* `twoFactorSecret`: The secret key used to generate TOTP codes.
* `twoFactorBackupCodes`: Encrypted backup codes for account recovery.
To add new tables and columns to your database, you have two options:
`CLI`: Use the migrate or generate command. These commands will scan your database and guide you through adding any missing tables or columns.
`Manual Method`: Follow the instructions in the plugin documentation to manually add tables and columns.
Both methods ensure your database schema stays up to date with your plugins' requirements.
# concepts: Email
URL: /docs/concepts/email
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/email.mdx
Learn how to use email with Better Auth.
***
title: Email
description: Learn how to use email with Better Auth.
-----------------------------------------------------
Email is a key part of Better Auth, required for all users regardless of their authentication method. Better Auth provides email and password authentication out of the box, and a lot of utilities to help you manage email verification, password reset, and more.
## Email Verification
Email verification is a security feature that ensures users provide a valid email address. It helps prevent spam and abuse by confirming that the email address belongs to the user. In this guide, you'll get a walk through of how to implement token based email verification in your app.
To use otp based email verification, check out the [OTP Verification](/docs/plugins/email-otp) guide.
### Adding Email Verification to Your App
To enable email verification, you need to pass a function that sends a verification email with a link.
* **sendVerificationEmail**: This function is triggered when email verification starts. It accepts a data object with the following properties:
* `user`: The user object containing the email address.
* `url`: The verification URL the user must click to verify their email.
* `token`: The verification token used to complete the email verification to be used when implementing a custom verification URL.
and a `request` object as the second parameter.
```ts title="auth.ts"
import { betterAuth } from 'better-auth';
import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
await sendEmail({
to: user.email,
subject: 'Verify your email address',
text: `Click the link to verify your email: ${url}`
})
}
}
})
```
### Triggering Email Verification
You can initiate email verification in several ways:
#### 1. During Sign-up
To automatically send a verification email at signup, set `emailVerification.sendOnSignUp` to `true`.
```ts title="auth.ts"
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
emailVerification: {
sendOnSignUp: true
}
})
```
This sends a verification email when a user signs up. For social logins, email verification status is read from the SSO.
With `sendOnSignUp` enabled, when the user logs in with an SSO that does not claim the email as verified, Better Auth will dispatch a verification email, but the verification is not required to login even when `requireEmailVerification` is enabled.
#### 2. Require Email Verification
If you enable require email verification, users must verify their email before they can log in. And every time a user tries to sign in, `sendVerificationEmail` is called.
This only works if you have `sendVerificationEmail` implemented and if the user is trying to sign in with email and password.
```ts title="auth.ts"
export const auth = betterAuth({
emailAndPassword: {
requireEmailVerification: true
}
})
```
if a user tries to sign in without verifying their email, you can handle the error and show a message to the user.
```ts title="auth-client.ts"
await authClient.signIn.email({
email: "email@example.com",
password: "password"
}, {
onError: (ctx) => {
// Handle the error
if(ctx.error.status === 403) {
alert("Please verify your email address")
}
//you can also show the original error message
alert(ctx.error.message)
}
})
```
#### 3. Manually
You can also manually trigger email verification by calling `sendVerificationEmail`.
```ts
await authClient.sendVerificationEmail({
email: "user@email.com",
callbackURL: "/" // The redirect URL after verification
})
```
### Verifying the Email
If the user clicks the provided verification URL, their email is automatically verified, and they are redirected to the `callbackURL`.
For manual verification, you can send the user a custom link with the `token` and call the `verifyEmail` function.
```ts
await authClient.verifyEmail({
query: {
token: "" // Pass the token here
}
})
```
### Auto Sign In After Verification
To sign in the user automatically after they successfully verify their email, set the `autoSignInAfterVerification` option to `true`:
```ts
const auth = betterAuth({
//...your other options
emailVerification: {
autoSignInAfterVerification: true
}
})
```
### Callback after successful email verification
You can run custom code immediately after a user verifies their email using the `afterEmailVerification` callback. This is useful for any side-effects you want to trigger, like granting access to special features or logging the event.
The `afterEmailVerification` function runs automatically when a user's email is confirmed, receiving the `user` object and `request` details so you can perform actions for that specific user.
Here's how you can set it up:
```ts title="auth.ts"
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
emailVerification: {
async afterEmailVerification(user, request) {
// Your custom logic here, e.g., grant access to premium features
console.log(`${user.email} has been successfully verified!`);
}
}
})
```
## Password Reset Email
Password reset allows users to reset their password if they forget it. Better Auth provides a simple way to implement password reset functionality.
You can enable password reset by passing a function that sends a password reset email with a link.
```ts title="auth.ts"
import { betterAuth } from 'better-auth';
import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url, token }, request) => {
await sendEmail({
to: user.email,
subject: 'Reset your password',
text: `Click the link to reset your password: ${url}`
})
}
}
})
```
Check out the [Email and Password](/docs/authentication/email-password#forget-password) guide for more details on how to implement password reset in your app.
Also you can check out the [Otp verification](/docs/plugins/email-otp#reset-password) guide for how to implement password reset with OTP in your app.
# concepts: Hooks
URL: /docs/concepts/hooks
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/hooks.mdx
Better Auth Hooks let you customize BetterAuth's behavior
***
title: Hooks
description: Better Auth Hooks let you customize BetterAuth's behavior
----------------------------------------------------------------------
Hooks in Better Auth let you "hook into" the lifecycle and execute custom logic. They provide a way to customize Better Auth's behavior without writing a full plugin.
We highly recommend using hooks if you need to make custom adjustments to an endpoint rather than making another endpoint outside of Better Auth.
## Before Hooks
**Before hooks** run *before* an endpoint is executed. Use them to modify requests, pre validate data, or return early.
### Example: Enforce Email Domain Restriction
This hook ensures that users can only sign up if their email ends with `@example.com`:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { createAuthMiddleware, APIError } from "better-auth/api";
export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path !== "/sign-up/email") {
return;
}
if (!ctx.body?.email.endsWith("@example.com")) {
throw new APIError("BAD_REQUEST", {
message: "Email must end with @example.com",
});
}
}),
},
});
```
### Example: Modify Request Context
To adjust the request context before proceeding:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path === "/sign-up/email") {
return {
context: {
...ctx,
body: {
...ctx.body,
name: "John Doe",
},
}
};
}
}),
},
});
```
## After Hooks
**After hooks** run *after* an endpoint is executed. Use them to modify responses.
### Example: Send a notification to your channel when a new user is registered
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
import { sendMessage } from "@/lib/notification"
export const auth = betterAuth({
hooks: {
after: createAuthMiddleware(async (ctx) => {
if(ctx.path.startsWith("/sign-up")){
const newSession = ctx.context.newSession;
if(newSession){
sendMessage({
type: "user-register",
name: newSession.user.name,
})
}
}
}),
},
});
```
## Ctx
When you call `createAuthMiddleware` a `ctx` object is passed that provides a lot of useful properties. Including:
* **Path:** `ctx.path` to get the current endpoint path.
* **Body:** `ctx.body` for parsed request body (available for POST requests).
* **Headers:** `ctx.headers` to access request headers.
* **Request:** `ctx.request` to access the request object (may not exist in server-only endpoints).
* **Query Parameters:** `ctx.query` to access query parameters.
* **Context**: `ctx.context` auth related context, useful for accessing new session, auth cookies configuration, password hashing, config...
and more.
### Request Response
This utilities allows you to get request information and to send response from a hook.
#### JSON Responses
Use `ctx.json` to send JSON responses:
```ts
const hook = createAuthMiddleware(async (ctx) => {
return ctx.json({
message: "Hello World",
});
});
```
#### Redirects
Use `ctx.redirect` to redirect users:
```ts
import { createAuthMiddleware } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
throw ctx.redirect("/sign-up/name");
});
```
#### Cookies
* Set cookies: `ctx.setCookies` or `ctx.setSignedCookie`.
* Get cookies: `ctx.getCookies` or `ctx.getSignedCookies`.
Example:
```ts
import { createAuthMiddleware } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
ctx.setCookies("my-cookie", "value");
await ctx.setSignedCookie("my-signed-cookie", "value", ctx.context.secret, {
maxAge: 1000,
});
const cookie = ctx.getCookies("my-cookie");
const signedCookie = await ctx.getSignedCookies("my-signed-cookie");
});
```
#### Errors
Throw errors with `APIError` for a specific status code and message:
```ts
import { createAuthMiddleware, APIError } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
throw new APIError("BAD_REQUEST", {
message: "Invalid request",
});
});
```
### Context
The `ctx` object contains another `context` object inside that's meant to hold contexts related to auth. Including a newly created session on after hook, cookies configuration, password hasher and so on.
#### New Session
The newly created session after an endpoint is run. This only exist in after hook.
```ts title="auth.ts"
createAuthMiddleware(async (ctx) => {
const newSession = ctx.context.newSession
});
```
#### Returned
The returned value from the hook is passed to the next hook in the chain.
```ts title="auth.ts"
createAuthMiddleware(async (ctx) => {
const returned = ctx.context.returned; //this could be a successful response or an APIError
});
```
#### Response Headers
The response headers added by endpoints and hooks that run before this hook.
```ts title="auth.ts"
createAuthMiddleware(async (ctx) => {
const responseHeaders = ctx.context.responseHeaders;
});
```
#### Predefined Auth Cookies
Access BetterAuth’s predefined cookie properties:
```ts title="auth.ts"
createAuthMiddleware(async (ctx) => {
const cookieName = ctx.context.authCookies.sessionToken.name;
});
```
#### Secret
You can access the `secret` for your auth instance on `ctx.context.secret`
#### Password
The password object provider `hash` and `verify`
* `ctx.context.password.hash`: let's you hash a given password.
* `ctx.context.password.verify`: let's you verify given `password` and a `hash`.
#### Adapter
Adapter exposes the adapter methods used by Better Auth. Including `findOne`, `findMany`, `create`, `delete`, `update` and `updateMany`. You generally should use your actually `db` instance from your orm rather than this adapter.
#### Internal Adapter
These are calls to your db that perform specific actions. `createUser`, `createSession`, `updateSession`...
This may be useful to use instead of using your db directly to get access to `databaseHooks`, proper `secondaryStorage` support and so on. If you're make a query similar to what exist in this internal adapter actions it's worth a look.
#### generateId
You can use `ctx.context.generateId` to generate Id for various reasons.
## Reusable Hooks
If you need to reuse a hook across multiple endpoints, consider creating a plugin. Learn more in the [Plugins Documentation](/docs/concepts/plugins).
# concepts: OAuth
URL: /docs/concepts/oauth
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/oauth.mdx
How Better Auth handles OAuth
***
title: OAuth
description: How Better Auth handles OAuth
------------------------------------------
Better Auth comes with built-in support for OAuth 2.0 and OpenID Connect. This allows you to authenticate users via popular OAuth providers like Google, Facebook, GitHub, and more.
If your desired provider isn't directly supported, you can use the [Generic OAuth Plugin](/docs/plugins/generic-oauth) for custom integrations.
## Configuring Social Providers
To enable a social provider, you need to provide `clientId` and `clientSecret` for the provider.
Here's an example of how to configure Google as a provider:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
// Other configurations...
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
},
},
});
```
## Usage
### Sign In
To sign in with a social provider, you can use the `signIn.social` function with the `authClient` or `auth.api` for server-side usage.
```ts
// client-side usage
await authClient.signIn.social({
provider: "google", // or any other provider id
})
```
```ts
// server-side usage
await auth.api.signInSocial({
body: {
provider: "google", // or any other provider id
},
});
```
### Link account
To link an account to a social provider, you can use the `linkAccount` function with the `authClient` or `auth.api` for server-side usage.
```ts
await authClient.linkSocial({
provider: "google", // or any other provider id
})
```
server-side usage:
```ts
await auth.api.linkSocialAccount({
body: {
provider: "google", // or any other provider id
},
headers: // pass headers with authenticated token
});
```
### Get Access Token
To get the access token for a social provider, you can use the `getAccessToken` function with the `authClient` or `auth.api` for server-side usage. When you use this endpoint, if the access token is expired, it will be refreshed.
```ts
const { accessToken } = await authClient.getAccessToken({
providerId: "google", // or any other provider id
accountId: "accountId", // optional, if you want to get the access token for a specific account
})
```
server-side usage:
```ts
await auth.api.getAccessToken({
body: {
providerId: "google", // or any other provider id
accountId: "accountId", // optional, if you want to get the access token for a specific account
userId: "userId", // optional, if you don't provide headers with authenticated token
},
headers: // pass headers with authenticated token
});
```
### Get Account Info Provided by the provider
To get provider specific account info you can use the `accountInfo` function with the `authClient` or `auth.api` for server-side usage.
```ts
const info = await authClient.accountInfo({
accountId: "accountId", // here you pass in the provider given account id, the provider is automatically detected from the account id
})
```
server-side usage:
```ts
await auth.api.accountInfo({
body: { accountId: "accountId" },
headers: // pass headers with authenticated token
});
```
### Requesting Additional Scopes
Sometimes your application may need additional OAuth scopes after the user has already signed up (e.g., for accessing GitHub repositories or Google Drive). Users may not want to grant extensive permissions initially, preferring to start with minimal permissions and grant additional access as needed.
You can request additional scopes by using the `linkSocial` method with the same provider. This will trigger a new OAuth flow that requests the additional scopes while maintaining the existing account connection.
```ts
const requestAdditionalScopes = async () => {
await authClient.linkSocial({
provider: "google",
scopes: ["https://www.googleapis.com/auth/drive.file"],
});
};
```
Make sure you're running Better Auth version 1.2.7 or later. Earlier versions (like 1.2.2) may show a "Social account already linked" error when trying to link with an existing provider for additional scopes.
### Other Provider Configurations
**scope** The scope of the access request. For example, `email` or `profile`.
**redirectURI** Custom redirect URI for the provider. By default, it uses `/api/auth/callback/${providerName}`.
**disableImplicitSignUp:** Disables implicit sign-up. In order to sign up a user, `requestSignUp` needs to be set to `true` when signing in.
**disableSignUp:** Disables sign-up for new users.
**disableIdTokenSignIn:** Disables the use of the ID token for sign-in. By default, it's enabled for some providers like Google and Apple.
**verifyIdToken** A custom function to verify the ID token.
**getUserInfo** A custom function to fetch user information from the provider. Given the tokens returned from the provider, this function should return the user's information.
**overrideUserInfoOnSignIn**: A boolean value that determines whether to override the user information in the database when signing in. By default, it is set to `false`, meaning that the user information will not be overridden during sign-in. If you want to update the user information every time they sign in, set this to `true`.
**refreshAccessToken**: A custom function to refresh the token. This feature is only supported for built-in social providers (Google, Facebook, GitHub, etc.) and is not currently supported for custom OAuth providers configured through the Generic OAuth Plugin. For built-in providers, you can provide a custom function to refresh the token if needed.
**mapProfileToUser** A custom function to map the user profile returned from the provider to the user object in your database.
Useful, if you have additional fields in your user object you want to populate from the provider's profile. Or if you want to change how by default the user object is mapped.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
// Other configurations...
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
mapProfileToUser: (profile) => {
return {
firstName: profile.given_name,
lastName: profile.family_name,
};
},
},
},
});
```
## How OAuth Works in Better Auth
Here's what happens when a user selects a provider to authenticate with:
1. **Configuration Check:** Ensure the necessary provider details (e.g., client ID, secret) are configured.
2. **State Generation:** Generate and save a state token in your database for CSRF protection.
3. **PKCE Support:** If applicable, create a PKCE code challenge and verifier for secure exchanges.
4. **Authorization URL Construction:** Build the provider's authorization URL with parameters like client ID, redirect URI, state, etc. The callback URL usually follows the pattern `/api/auth/callback/${providerName}`.
5. **User Redirection:**
* If redirection is enabled, users are redirected to the provider's login page.
* If redirection is disabled, the authorization URL is returned for the client to handle the redirection.
### Post-Login Flow
After the user completes the login process, the provider redirects them back to the callback URL with a code and state. Better Auth handles the rest:
1. **Token Exchange:** The code is exchanged for an access token and user information.
2. **User Handling:**
* If the user doesn't exist, a new account is created.
* If the user exists, they are logged in.
* If the user has multiple accounts across providers, Better Auth links them based on your configuration. Learn more about [account linking](/docs/concepts/users-accounts#account-linking).
3. **Session Creation:** A new session is created for the user.
4. **Redirect:** Users are redirected to the specified URL provided during the initial request or `/`.
If any error occurs during the process, Better Auth handles it and redirects the user to the error URL (if provided) or the callbackURL. And it includes the error message in the query string `?error=...`.
# concepts: Plugins
URL: /docs/concepts/plugins
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/plugins.mdx
Learn how to use plugins with Better Auth.
***
title: Plugins
description: Learn how to use plugins with Better Auth.
-------------------------------------------------------
Plugins are a key part of Better Auth, they let you extend the base functionalities. You can use them to add new authentication methods, features, or customize behaviors.
Better Auth comes with many built-in plugins ready to use. Check the plugins section for details. You can also create your own plugins.
## Using a Plugin
Plugins can be a server-side plugin, a client-side plugin, or both.
To add a plugin on the server, include it in the `plugins` array in your auth configuration. The plugin will initialize with the provided options.
```ts title="server.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [
// Add your plugins here
]
});
```
Client plugins are added when creating the client. Most plugin require both server and client plugins to work correctly.
The Better Auth auth client on the frontend uses the `createAuthClient` function provided by `better-auth/client`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient({
plugins: [
// Add your client plugins here
]
});
```
We recommend keeping the auth-client and your normal auth instance in separate files.
## Creating a Plugin
To get started, you'll need a server plugin.
Server plugins are the backbone of all plugins, and client plugins are there to provide an interface with frontend APIs to easily work with your server plugins.
If your server plugins has endpoints that needs to be called from the client, you'll also need to create a client plugin.
### What can a plugin do?
* Create custom `endpoint`s to perform any action you want.
* Extend database tables with custom `schemas`.
* Use a `middleware` to target a group of routes using it's route matcher, and run only when those routes are called through a request.
* Use `hooks` to target a specific route or request. And if you want to run the hook even if the endpoint is called directly.
* Use `onRequest` or `onResponse` if you want to do something that affects all requests or responses.
* Create custom `rate-limit` rule.
## Create a Server plugin
To create a server plugin you need to pass an object that satisfies the `BetterAuthPlugin` interface.
The only required property is `id`, which is a unique identifier for the plugin.
Both server and client plugins can use the same `id`.
```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
export const myPlugin = ()=>{
return {
id: "my-plugin",
} satisfies BetterAuthPlugin
}
```
You don't have to make the plugin a function, but it's recommended to do so. This way you can pass options to the plugin and it's consistent with the built-in plugins.
### Endpoints
To add endpoints to the server, you can pass `endpoints` which requires an object with the key being any `string` and the value being an `AuthEndpoint`.
To create an Auth Endpoint you'll need to import `createAuthEndpoint` from `better-auth`.
Better Auth uses wraps around another library called Better Call to create endpoints. Better call is a simple ts web framework made by the same team behind Better Auth.
```ts title="plugin.ts"
import { createAuthEndpoint } from "better-auth/api";
const myPlugin = ()=> {
return {
id: "my-plugin",
endpoints: {
getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
method: "GET",
}, async(ctx) => {
return ctx.json({
message: "Hello World"
})
})
}
} satisfies BetterAuthPlugin
}
```
Create Auth endpoints wraps around `createEndpoint` from Better Call. Inside the `ctx` object, it'll provide another object called `context` that give you access better-auth specific contexts including `options`, `db`, `baseURL` and more.
**Context Object**
* `appName`: The name of the application. Defaults to "Better Auth".
* `options`: The options passed to the Better Auth instance.
* `tables`: Core tables definition. It is an object which has the table name as the key and the schema definition as the value.
* `baseURL`: the baseURL of the auth server. This includes the path. For example, if the server is running on `http://localhost:3000`, the baseURL will be `http://localhost:3000/api/auth` by default unless changed by the user.
* `session`: The session configuration. Includes `updateAge` and `expiresIn` values.
* `secret`: The secret key used for various purposes. This is defined by the user.
* `authCookie`: The default cookie configuration for core auth cookies.
* `logger`: The logger instance used by Better Auth.
* `db`: The Kysely instance used by Better Auth to interact with the database.
* `adapter`: This is the same as db but it give you `orm` like functions to interact with the database. (we recommend using this over `db` unless you need raw sql queries or for performance reasons)
* `internalAdapter`: These are internal db calls that are used by Better Auth. For example, you can use these calls to create a session instead of using `adapter` directly. `internalAdapter.createSession(userId)`
* `createAuthCookie`: This is a helper function that let's you get a cookie `name` and `options` for either to `set` or `get` cookies. It implements things like `__secure` prefix and `__host` prefix for cookies based on
For other properties, you can check the Better Call documentation and the source code .
**Rules for Endpoints**
* Makes sure you use kebab-case for the endpoint path
* Make sure to only use `POST` or `GET` methods for the endpoints.
* Any function that modifies a data should be a `POST` method.
* Any function that fetches data should be a `GET` method.
* Make sure to use the `createAuthEndpoint` function to create API endpoints.
* Make sure your paths are unique to avoid conflicts with other plugins. If you're using a common path, add the plugin name as a prefix to the path. (`/my-plugin/hello-world` instead of `/hello-world`.)
### Schema
You can define a database schema for your plugin by passing a `schema` object. The schema object should have the table name as the key and the schema definition as the value.
```ts title="plugin.ts"
import { BetterAuthPlugin } from "better-auth/plugins";
const myPlugin = ()=> {
return {
id: "my-plugin",
schema: {
myTable: {
fields: {
name: {
type: "string"
}
},
modelName: "myTable" // optional if you want to use a different name than the key
}
}
} satisfies BetterAuthPlugin
}
```
**Fields**
By default better-auth will create an `id` field for each table. You can add additional fields to the table by adding them to the `fields` object.
The key is the column name and the value is the column definition. The column definition can have the following properties:
`type`: The type of the field. It can be `string`, `number`, `boolean`, `date`.
`required`: if the field should be required on a new record. (default: `false`)
`unique`: if the field should be unique. (default: `false`)
`reference`: if the field is a reference to another table. (default: `null`) It takes an object with the following properties:
* `model`: The table name to reference.
* `field`: The field name to reference.
* `onDelete`: The action to take when the referenced record is deleted. (default: `null`)
**Other Schema Properties**
`disableMigration`: if the table should not be migrated. (default: `false`)
```ts title="plugin.ts"
const myPlugin = (opts: PluginOptions)=>{
return {
id: "my-plugin",
schema: {
rateLimit: {
fields: {
key: {
type: "string",
},
},
disableMigration: opts.storage.provider !== "database", // [!code highlight]
},
},
} satisfies BetterAuthPlugin
}
```
if you add additional fields to a `user` or `session` table, the types will be inferred automatically on `getSession` and `signUpEmail` calls.
```ts title="plugin.ts"
const myPlugin = ()=>{
return {
id: "my-plugin",
schema: {
user: {
fields: {
age: {
type: "number",
},
},
},
},
} satisfies BetterAuthPlugin
}
```
This will add an `age` field to the `user` table and all `user` returning endpoints will include the `age` field and it'll be inferred properly by typescript.
Don't store sensitive information in `user` or `session` table. Crate a new table if you need to store sensitive information.
### Hooks
Hooks are used to run code before or after an action is performed, either from a client or directly on the server. You can add hooks to the server by passing a `hooks` object, which should contain `before` and `after` properties.
```ts title="plugin.ts"
import { createAuthMiddleware } from "better-auth/plugins";
const myPlugin = ()=>{
return {
id: "my-plugin",
hooks: {
before: [{
matcher: (context)=>{
return context.headers.get("x-my-header") === "my-value"
},
handler: createAuthMiddleware(async (ctx)=>{
//do something before the request
return {
context: ctx // if you want to modify the context
}
})
}],
after: [{
matcher: (context)=>{
return context.path === "/sign-up/email"
},
handler: createAuthMiddleware(async (ctx)=>{
return ctx.json({
message: "Hello World"
}) // if you want to modify the response
})
}]
}
} satisfies BetterAuthPlugin
}
```
### Middleware
You can add middleware to the server by passing a `middlewares` array. This array should contain middleware objects, each with a `path` and a `middleware` property. Unlike hooks, middleware only runs on `api` requests from a client. If the endpoint is invoked directly, the middleware will not run.
The `path` can be either a string or a path matcher, using the same path-matching system as `better-call`.
If you throw an `APIError` from the middleware or returned a `Response` object, the request will be stopped and the response will be sent to the client.
```ts title="plugin.ts"
const myPlugin = ()=>{
return {
id: "my-plugin",
middlewares: [
{
path: "/my-plugin/hello-world",
middleware: createAuthMiddleware(async(ctx)=>{
//do something
})
}
]
} satisfies BetterAuthPlugin
}
```
### On Request & On Response
Additional to middlewares, you can also hook into right before a request is made and right after a response is returned. This is mostly useful if you want to do something that affects all requests or responses.
#### On Request
The `onRequest` function is called right before the request is made. It takes two parameters: the `request` and the `context` object.
Here’s how it works:
* **Continue as Normal**: If you don't return anything, the request will proceed as usual.
* **Interrupt the Request**: To stop the request and send a response, return an object with a `response` property that contains a `Response` object.
* **Modify the Request**: You can also return a modified `request` object to change the request before it's sent.
```ts title="plugin.ts"
const myPlugin = ()=> {
return {
id: "my-plugin",
onRequest: async (request, context) => {
//do something
},
} satisfies BetterAuthPlugin
}
```
#### On Response
The `onResponse` function is executed immediately after a response is returned. It takes two parameters: the `response` and the `context` object.
Here’s how to use it:
* **Modify the Response**: You can return a modified response object to change the response before it is sent to the client.
* **Continue Normally**: If you don't return anything, the response will be sent as is.
```ts title="plugin.ts"
const myPlugin = ()=>{
return {
id: "my-plugin",
onResponse: async (response, context) => {
//do something
},
} satisfies BetterAuthPlugin
}
```
### Rate Limit
You can define custom rate limit rules for your plugin by passing a `rateLimit` array. The rate limit array should contain an array of rate limit objects.
```ts title="plugin.ts"
const myPlugin = ()=>{
return {
id: "my-plugin",
rateLimit: [
{
pathMatcher: (path)=>{
return path === "/my-plugin/hello-world"
},
limit: 10,
window: 60,
}
]
} satisfies BetterAuthPlugin
}
```
### Server-plugin helper functions
Some additional helper functions for creating server plugins.
#### `getSessionFromCtx`
Allows you to get the client's session data by passing the auth middleware's `context`.
```ts title="plugin.ts"
import { createAuthMiddleware } from "better-auth/plugins";
const myPlugin = {
id: "my-plugin",
hooks: {
before: [{
matcher: (context)=>{
return context.headers.get("x-my-header") === "my-value"
},
handler: createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
//do something with the client's session.
return {
context: ctx
}
})
}],
}
} satisfies BetterAuthPlugin
```
#### `sessionMiddleware`
A middleware that checks if the client has a valid session. If the client has a valid session, it'll add the session data to the context object.
```ts title="plugin.ts"
import { createAuthMiddleware } from "better-auth/plugins";
import { sessionMiddleware } from "better-auth/api";
const myPlugin = ()=>{
return {
id: "my-plugin",
endpoints: {
getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
method: "GET",
use: [sessionMiddleware], // [!code highlight]
}, async(ctx) => {
const session = ctx.context.session;
return ctx.json({
message: "Hello World"
})
})
}
} satisfies BetterAuthPlugin
}
```
## Creating a client plugin
If your endpoints needs to be called from the client, you'll need to also create a client plugin. Better Auth clients can infer the endpoints from the server plugins. You can also add additional client side logic.
```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth";
export const myPluginClient = ()=>{
return {
id: "my-plugin",
} satisfies BetterAuthClientPlugin
}
```
### Endpoint Interface
Endpoints are inferred from the server plugin by adding a `$InferServerPlugin` key to the client plugin.
The client infers the `path` as an object and converts kebab-case to camelCase. For example, `/my-plugin/hello-world` becomes `myPlugin.helloWorld`.
```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";
const myPluginClient = ()=> {
return {
id: "my-plugin",
$InferServerPlugin: {} as ReturnType,
} satisfies BetterAuthClientPlugin
}
```
### Get actions
If you need to add additional methods or what not to the client you can use the `getActions` function. This function is called with the `fetch` function from the client.
Better Auth uses Better fetch to make requests. Better fetch is a simple fetch wrapper made by the same author of Better Auth.
```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";
import type { BetterFetchOption } from "@better-fetch/fetch";
const myPluginClient = {
id: "my-plugin",
$InferServerPlugin: {} as ReturnType,
getActions: ($fetch)=>{
return {
myCustomAction: async (data: {
foo: string,
}, fetchOptions?: BetterFetchOption)=>{
const res = $fetch("/custom/action", {
method: "POST",
body: {
foo: data.foo
},
...fetchOptions
})
return res
}
}
}
} satisfies BetterAuthClientPlugin
```
As a general guideline, ensure that each function accepts only one argument, with an optional second argument for fetchOptions to allow users to pass additional options to the fetch call. The function should return an object containing data and error keys.
If your use case involves actions beyond API calls, feel free to deviate from this rule.
### Get Atoms
This is only useful if you want to provide `hooks` like `useSession`.
Get atoms is called with the `fetch` function from better fetch and it should return an object with the atoms. The atoms should be created using nanostores. The atoms will be resolved by each framework `useStore` hook provided by nanostores.
```ts title="client-plugin.ts"
import { atom } from "nanostores";
import type { BetterAuthClientPlugin } from "better-auth/client";
const myPluginClient = {
id: "my-plugin",
$InferServerPlugin: {} as ReturnType,
getAtoms: ($fetch)=>{
const myAtom = atom()
return {
myAtom
}
}
} satisfies BetterAuthClientPlugin
```
See built-in plugins for examples of how to use atoms properly.
### Path methods
By default, inferred paths use `GET` method if they don't require a body and `POST` if they do. You can override this by passing a `pathMethods` object. The key should be the path and the value should be the method ("POST" | "GET").
```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";
const myPluginClient = {
id: "my-plugin",
$InferServerPlugin: {} as ReturnType,
pathMethods: {
"/my-plugin/hello-world": "POST"
}
} satisfies BetterAuthClientPlugin
```
### Fetch plugins
If you need to use better fetch plugins you can pass them to the `fetchPlugins` array. You can read more about better fetch plugins in the better fetch documentation.
### Atom Listeners
This is only useful if you want to provide `hooks` like `useSession` and you want to listen to atoms and re-evaluate them when they change.
You can see how this is used in the built-in plugins.
# concepts: Rate Limit
URL: /docs/concepts/rate-limit
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/rate-limit.mdx
How to limit the number of requests a user can make to the server in a given time period.
***
title: Rate Limit
description: How to limit the number of requests a user can make to the server in a given time period.
------------------------------------------------------------------------------------------------------
Better Auth includes a built-in rate limiter to help manage traffic and prevent abuse. By default, in production mode, the rate limiter is set to:
* Window: 60 seconds
* Max Requests: 100 requests
Server-side requests made using `auth.api` aren't affected by rate limiting. Rate limits only apply to client-initiated requests.
You can easily customize these settings by passing the rateLimit object to the betterAuth function.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
window: 10, // time window in seconds
max: 100, // max requests in the window
},
})
```
Rate limiting is disabled in development mode by default. In order to enable it, set `enabled` to `true`:
```ts title="auth.ts"
export const auth = betterAuth({
rateLimit: {
enabled: true,
//...other options
},
})
```
In addition to the default settings, Better Auth provides custom rules for specific paths. For example:
* `/sign-in/email`: Is limited to 3 requests within 10 seconds.
In addition, plugins also define custom rules for specific paths. For example, `twoFactor` plugin has custom rules:
* `/two-factor/verify`: Is limited to 3 requests within 10 seconds.
These custom rules ensure that sensitive operations are protected with stricter limits.
## Configuring Rate Limit
### Connecting IP Address
Rate limiting uses the connecting IP address to track the number of requests made by a user. The
default header checked is `x-forwarded-for`, which is commonly used in production environments. If
you are using a different header to track the user's IP address, you'll need to specify it.
```ts title="auth.ts"
export const auth = betterAuth({
//...other options
advanced: {
ipAddress: {
ipAddressHeaders: ["cf-connecting-ip"], // Cloudflare specific header example
},
},
rateLimit: {
enabled: true,
window: 60, // time window in seconds
max: 100, // max requests in the window
},
})
```
### Rate Limit Window
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
window: 60, // time window in seconds
max: 100, // max requests in the window
},
})
```
You can also pass custom rules for specific paths.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
window: 60, // time window in seconds
max: 100, // max requests in the window
customRules: {
"/sign-in/email": {
window: 10,
max: 3,
},
"/two-factor/*": async (request)=> {
// custom function to return rate limit window and max
return {
window: 10,
max: 3,
}
}
},
},
})
```
If you like to disable rate limiting for a specific path, you can set it to `false` or return `false` from the custom rule function.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
customRules: {
"/get-session": false,
},
},
})
```
### Storage
By default, rate limit data is stored in memory, which may not be suitable for many use cases, particularly in serverless environments. To address this, you can use a database, secondary storage, or custom storage for storing rate limit data.
**Using Database**
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
storage: "database",
modelName: "rateLimit", //optional by default "rateLimit" is used
},
})
```
Make sure to run `migrate` to create the rate limit table in your database.
```bash
npx @better-auth/cli migrate
```
**Using Secondary Storage**
If a [Secondary Storage](/docs/concepts/database#secondary-storage) has been configured you can use that to store rate limit data.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
storage: "secondary-storage"
},
})
```
**Custom Storage**
If none of the above solutions suits your use case you can implement a `customStorage`.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
customStorage: {
get: async (key) => {
// get rate limit data
},
set: async (key, value) => {
// set rate limit data
},
},
},
})
```
## Handling Rate Limit Errors
When a request exceeds the rate limit, Better Auth returns the following header:
* `X-Retry-After`: The number of seconds until the user can make another request.
To handle rate limit errors on the client side, you can manage them either globally or on a per-request basis. Since Better Auth clients wrap over Better Fetch, you can pass `fetchOptions` to handle rate limit errors
**Global Handling**
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
fetchOptions: {
onError: async (context) => {
const { response } = context;
if (response.status === 429) {
const retryAfter = response.headers.get("X-Retry-After");
console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
}
},
}
})
```
**Per Request Handling**
```ts title="auth-client.ts"
import { authClient } from "./auth-client";
await authClient.signIn.email({
fetchOptions: {
onError: async (context) => {
const { response } = context;
if (response.status === 429) {
const retryAfter = response.headers.get("X-Retry-After");
console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
}
},
}
})
```
### Schema
If you are using a database to store rate limit data you need this schema:
Table Name: `rateLimit`
# concepts: Session Management
URL: /docs/concepts/session-management
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/session-management.mdx
Better Auth session management.
***
title: Session Management
description: Better Auth session management.
--------------------------------------------
Better Auth manages session using a traditional cookie-based session management. The session is stored in a cookie and is sent to the server on every request. The server then verifies the session and returns the user data if the session is valid.
## Session table
The session table stores the session data. The session table has the following fields:
* `id`: The session token. Which is also used as the session cookie.
* `userId`: The user ID of the user.
* `expiresAt`: The expiration date of the session.
* `ipAddress`: The IP address of the user.
* `userAgent`: The user agent of the user. It stores the user agent header from the request.
## Session Expiration
The session expires after 7 days by default. But whenever the session is used and the `updateAge` is reached, the session expiration is updated to the current time plus the `expiresIn` value.
You can change both the `expiresIn` and `updateAge` values by passing the `session` object to the `auth` configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... other config options
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24 // 1 day (every 1 day the session expiration is updated)
}
})
```
### Disable Session Refresh
You can disable session refresh so that the session is not updated regardless of the `updateAge` option.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... other config options
session: {
disableSessionRefresh: true
}
})
```
## Session Freshness
Some endpoints in Better Auth require the session to be **fresh**. A session is considered fresh if its `createdAt` is within the `freshAge` limit. By default, the `freshAge` is set to **1 day** (60 \* 60 \* 24).
You can customize the `freshAge` value by passing a `session` object in the `auth` configuration:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... other config options
session: {
freshAge: 60 * 5 // 5 minutes (the session is fresh if created within the last 5 minutes)
}
})
```
To **disable the freshness check**, set `freshAge` to `0`:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... other config options
session: {
freshAge: 0 // Disable freshness check
}
})
```
## Session Management
Better Auth provides a set of functions to manage sessions.
### Get Session
The `getSession` function retrieves the current active session.
```ts client="client.ts"
import { authClient } from "@/lib/client"
const { data: session } = await authClient.getSession()
```
To learn how to customize the session response check the [Customizing Session Response](#customizing-session-response) section.
### Use Session
The `useSession` action provides a reactive way to access the current session.
```ts client="client.ts"
import { authClient } from "@/lib/client"
const { data: session } = authClient.useSession()
```
### List Sessions
The `listSessions` function returns a list of sessions that are active for the user.
```ts title="auth-client.ts"
import { authClient } from "@/lib/client"
const sessions = await authClient.listSessions()
```
### Revoke Session
When a user signs out of a device, the session is automatically ended. However, you can also end a session manually from any device the user is signed into.
To end a session, use the `revokeSession` function. Just pass the session token as a parameter.
```ts title="auth-client.ts"
await authClient.revokeSession({
token: "session-token"
})
```
### Revoke Other Sessions
To revoke all other sessions except the current session, you can use the `revokeOtherSessions` function.
```ts title="auth-client.ts"
await authClient.revokeOtherSessions()
```
### Revoke All Sessions
To revoke all sessions, you can use the `revokeSessions` function.
```ts title="auth-client.ts"
await authClient.revokeSessions()
```
### Revoking Sessions on Password Change
You can revoke all sessions when the user changes their password by passing `revokeOtherSessions` as true on `changePassword` function.
```ts title="auth.ts"
await authClient.changePassword({
newPassword: newPassword,
currentPassword: currentPassword,
revokeOtherSessions: true,
})
```
## Session Caching
### Cookie Cache
Calling your database every time `useSession` or `getSession` invoked isn’t ideal, especially if sessions don’t change frequently. Cookie caching handles this by storing session data in a short-lived, signed cookie—similar to how JWT access tokens are used with refresh tokens.
When cookie caching is enabled, the server can check session validity from the cookie itself instead of hitting the database each time. The cookie is signed to prevent tampering, and a short `maxAge` ensures that the session data gets refreshed regularly. If a session is revoked or expires, the cookie will be invalidated automatically.
To turn on cookie caching, just set `session.cookieCache` in your auth config:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60 // Cache duration in seconds
}
}
});
```
If you want to disable returning from the cookie cache when fetching the session, you can pass `disableCookieCache:true` this will force the server to fetch the session from the database and also refresh the cookie cache.
```ts title="auth-client.ts"
const session = await authClient.getSession({ query: {
disableCookieCache: true
}})
```
or on the server
```ts title="server.ts"
await auth.api.getSession({
query: {
disableCookieCache: true,
},
headers: req.headers, // pass the headers
});
```
## Customizing Session Response
When you call `getSession` or `useSession`, the session data is returned as a `user` and `session` object. You can customize this response using the `customSession` plugin.
```ts title="auth.ts"
import { customSession } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
customSession(async ({ user, session }) => {
const roles = findUserRoles(session.session.userId);
return {
roles,
user: {
...user,
newField: "newField",
},
session
};
}),
],
});
```
This will add `roles` and `user.newField` to the session response.
**Infer on the Client**
```ts title="auth-client.ts"
import { customSessionClient } from "better-auth/client/plugins";
import type { auth } from "@/lib/auth"; // Import the auth instance as a type
const authClient = createAuthClient({
plugins: [customSessionClient()],
});
const { data } = authClient.useSession();
const { data: sessionData } = await authClient.getSession();
// data.roles
// data.user.newField
```
### Caveats on Customizing Session Response
1. The passed `session` object to the callback does not infer fields added by plugins.
However, as a workaround, you can pull up your auth options and pass it to the plugin to infer the fields.
```ts
import { betterAuth, BetterAuthOptions } from "better-auth";
const options = {
//...config options
plugins: [
//...plugins
]
} satisfies BetterAuthOptions;
export const auth = betterAuth({
...options,
plugins: [
...(options.plugins ?? []),
customSession(async ({ user, session }, ctx) => {
// now both user and session will infer the fields added by plugins and your custom fields
return {
user,
session
}
}, options), // pass options here // [!code highlight]
]
})
```
2. When your server and client code are in separate projects or repositories, and you cannot import the `auth` instance as a type reference, type inference for custom session fields will not work on the client side.
3. Session caching, including secondary storage or cookie cache, does not include custom fields. Each time the session is fetched, your custom session function will be called.
**Mutating the list-device-sessions endpoint**
The `/multi-session/list-device-sessions` endpoint from the [multi-session](/docs/plugins/multi-session) plugin is used to list the devices that the user is signed into.
You can mutate the response of this endpoint by passing the `shouldMutateListDeviceSessionsEndpoint` option to the `customSession` plugin.
By default, we do not mutate the response of this endpoint.
```ts title="auth.ts"
import { customSession } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
customSession(async ({ user, session }, ctx) => {
return {
user,
session
}
}, {}, { shouldMutateListDeviceSessionsEndpoint: true }), // [!code highlight]
],
});
```
# concepts: TypeScript
URL: /docs/concepts/typescript
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/typescript.mdx
Better Auth TypeScript integration.
***
title: TypeScript
description: Better Auth TypeScript integration.
------------------------------------------------
Better Auth is designed to be type-safe. Both the client and server are built with TypeScript, allowing you to easily infer types.
## TypeScript Config
### Strict Mode
Better Auth is designed to work with TypeScript's strict mode. We recommend enabling strict mode in your TypeScript config file:
```json title="tsconfig.json"
{
"compilerOptions": {
"strict": true
}
}
```
if you can't set `strict` to `true`, you can enable `strictNullChecks`:
```json title="tsconfig.json"
{
"compilerOptions": {
"strictNullChecks": true,
}
}
```
If you're running into issues with TypeScript inference exceeding maximum length the compiler will serialize,
then please make sure you're following the instructions above, as well as ensuring that both `declaration` and `composite` are not enabled.
## Inferring Types
Both the client SDK and the server offer types that can be inferred using the `$Infer` property. Plugins can extend base types like `User` and `Session`, and you can use `$Infer` to infer these types. Additionally, plugins can provide extra types that can also be inferred through `$Infer`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
export type Session = typeof authClient.$Infer.Session
```
The `Session` type includes both `session` and `user` properties. The user property represents the user object type, and the `session` property represents the `session` object type.
You can also infer types on the server side.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import Database from "better-sqlite3"
export const auth = betterAuth({
database: new Database("database.db")
})
type Session = typeof auth.$Infer.Session
```
## Additional Fields
Better Auth allows you to add additional fields to the user and session objects. All additional fields are properly inferred and available on the server and client side.
```ts
import { betterAuth } from "better-auth"
import Database from "better-sqlite3"
export const auth = betterAuth({
database: new Database("database.db"),
user: {
additionalFields: {
role: {
type: "string",
input: false
}
}
}
})
type Session = typeof auth.$Infer.Session
```
In the example above, we added a `role` field to the user object. This field is now available on the `Session` type.
### The `input` property
The `input` property in an additional field configuration determines whether the field should be included in the user input. This property defaults to `true`, meaning the field will be part of the user input during operations like registration.
To prevent a field from being part of the user input, you must explicitly set `input: false`:
```ts
additionalFields: {
role: {
type: "string",
input: false
}
}
```
When `input` is set to `false`, the field will be excluded from user input, preventing users from passing a value for it.
By default, additional fields are included in the user input, which can lead to security vulnerabilities if not handled carefully. For fields that should not be set by the user, like a `role`, it is crucial to set `input: false` in the configuration.
### Inferring Additional Fields on Client
To make sure proper type inference for additional fields on the client side, you need to inform the client about these fields. There are two approaches to achieve this, depending on your project structure:
1. For Monorepo or Single-Project Setups
If your server and client code reside in the same project, you can use the `inferAdditionalFields` plugin to automatically infer the additional fields from your server configuration.
```ts
import { inferAdditionalFields } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import type { auth } from "./auth";
export const authClient = createAuthClient({
plugins: [inferAdditionalFields()],
});
```
2. For Separate Client-Server Projects
If your client and server are in separate projects, you'll need to manually specify the additional fields when creating the auth client.
```ts
import { inferAdditionalFields } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [inferAdditionalFields({
user: {
role: {
type: "string"
}
}
})],
});
```
# concepts: User & Accounts
URL: /docs/concepts/users-accounts
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/users-accounts.mdx
User and account management.
***
title: User & Accounts
description: User and account management.
-----------------------------------------
Beyond authenticating users, Better Auth also provides a set of methods to manage users. This includes, updating user information, changing passwords, and more.
The user table stores the authentication data of the user [Click here to view the schema](/docs/concepts/database#user).
The user table can be extended using [additional fields](/docs/concepts/database#extending-core-schema) or by plugins to store additional data.
## Update User
### Update User Information
To update user information, you can use the `updateUser` function provided by the client. The `updateUser` function takes an object with the following properties:
```ts
await authClient.updateUser({
image: "https://example.com/image.jpg",
name: "John Doe",
})
```
### Change Email
To allow users to change their email, first enable the `changeEmail` feature, which is disabled by default. Set `changeEmail.enabled` to `true`:
```ts
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
}
}
})
```
For users with a verified email, provide the `sendChangeEmailVerification` function. This function triggers when a user changes their email, sending a verification email with a URL and token. If the current email isn't verified, the change happens immediately without verification.
```ts
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => {
await sendEmail({
to: user.email, // verification email must be sent to the current user email to approve the change
subject: 'Approve email change',
text: `Click the link to approve the change: ${url}`
})
}
}
}
})
```
Once enabled, use the `changeEmail` function on the client to update a user’s email. The user must verify their current email before changing it.
```ts
await authClient.changeEmail({
newEmail: "new-email@email.com",
callbackURL: "/dashboard", //to redirect after verification
});
```
After verification, the new email is updated in the user table, and a confirmation is sent to the new address.
If the current email is unverified, the new email is updated without the verification step.
### Change Password
A user's password isn't stored in the user table. Instead, it's stored in the account table. To change the password of a user, you can use one of the following approaches:
### Client Side
```ts
const { data, error } = await authClient.changePassword({
newPassword: newpassword1234,
currentPassword: oldpassword1234,
revokeOtherSessions, // required
});
```
### Server Side
```ts
const data = await auth.api.changePassword({
body: {
newPassword: newpassword1234,
currentPassword: oldpassword1234,
revokeOtherSessions, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type changePassword = {
/**
* The new password to set
*/
newPassword: string = "newpassword1234"
/**
* The current user password
*/
currentPassword: string = "oldpassword1234"
/**
* When set to true, all other active sessions for this user will be invalidated
*/
revokeOtherSessions?: boolean = true
}
```
### Set Password
If a user was registered using OAuth or other providers, they won't have a password or a credential account. In this case, you can use the `setPassword` action to set a password for the user. For security reasons, this function can only be called from the server. We recommend having users go through a 'forgot password' flow to set a password for their account.
```ts
await auth.api.setPassword({
body: { newPassword: "password" },
headers: // headers containing the user's session token
});
```
## Delete User
Better Auth provides a utility to hard delete a user from your database. It's disabled by default, but you can enable it easily by passing `enabled:true`
```ts
export const auth = betterAuth({
//...other config
user: {
deleteUser: { // [!code highlight]
enabled: true // [!code highlight]
} // [!code highlight]
}
})
```
Once enabled, you can call `authClient.deleteUser` to permanently delete user data from your database.
### Adding Verification Before Deletion
For added security, you’ll likely want to confirm the user’s intent before deleting their account. A common approach is to send a verification email. Better Auth provides a `sendDeleteAccountVerification` utility for this purpose.
This is especially needed if you have OAuth setup and want them to be able to delete their account without forcing them to login again for a fresh session.
Here’s how you can set it up:
```ts
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
sendDeleteAccountVerification: async (
{
user, // The user object
url, // The auto-generated URL for deletion
token // The verification token (can be used to generate custom URL)
},
request // The original request object (optional)
) => {
// Your email sending logic here
// Example: sendEmail(data.user.email, "Verify Deletion", data.url);
},
},
},
});
```
**How callback verification works:**
* **Callback URL**: The URL provided in `sendDeleteAccountVerification` is a pre-generated link that deletes the user data when accessed.
```ts title="delete-user.ts"
await authClient.deleteUser({
callbackURL: "/goodbye" // you can provide a callback URL to redirect after deletion
});
```
* **Authentication Check**: The user must be signed in to the account they’re attempting to delete.
If they aren’t signed in, the deletion process will fail.
If you have sent a custom URL, you can use the `deleteUser` method with the token to delete the user.
```ts title="delete-user.ts"
await authClient.deleteUser({
token
});
```
### Authentication Requirements
To delete a user, the user must meet one of the following requirements:
1. A valid password
if the user has a password, they can delete their account by providing the password.
```ts title="delete-user.ts"
await authClient.deleteUser({
password: "password"
});
```
2. Fresh session
The user must have a `fresh` session token, meaning the user must have signed in recently. This is checked if the password is not provided.
By default `session.freshAge` is set to `60 * 60 * 24` (1 day). You can change this value by passing the `session` object to the `auth` configuration. If it is set to `0`, the freshness check is disabled. It is recommended not to disable this check if you are not using email verification for deleting the account.
```ts title="delete-user.ts"
await authClient.deleteUser();
```
3. Enabled email verification (needed for OAuth users)
As OAuth users don't have a password, we need to send a verification email to confirm the user's intent to delete their account. If you have already added the `sendDeleteAccountVerification` callback, you can just call the `deleteUser` method without providing any other information.
```ts title="delete-user.ts"
await authClient.deleteUser();
```
4. If you have a custom delete account page and sent that url via the `sendDeleteAccountVerification` callback.
Then you need to call the `deleteUser` method with the token to complete the deletion.
```ts title="delete-user.ts"
await authClient.deleteUser({
token
});
```
### Callbacks
**beforeDelete**: This callback is called before the user is deleted. You can use this callback to perform any cleanup or additional checks before deleting the user.
```ts title="auth.ts"
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
beforeDelete: async (user) => {
// Perform any cleanup or additional checks here
},
},
},
});
```
you can also throw `APIError` to interrupt the deletion process.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
beforeDelete: async (user, request) => {
if (user.email.includes("admin")) {
throw new APIError("BAD_REQUEST", {
message: "Admin accounts can't be deleted",
});
}
},
},
},
});
```
**afterDelete**: This callback is called after the user is deleted. You can use this callback to perform any cleanup or additional actions after the user is deleted.
```ts title="auth.ts"
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
afterDelete: async (user, request) => {
// Perform any cleanup or additional actions here
},
},
},
});
```
## Accounts
Better Auth supports multiple authentication methods. Each authentication method is called a provider. For example, email and password authentication is a provider, Google authentication is a provider, etc.
When a user signs in using a provider, an account is created for the user. The account stores the authentication data returned by the provider. This data includes the access token, refresh token, and other information returned by the provider.
The account table stores the authentication data of the user [Click here to view the schema](/docs/concepts/database#account)
### List User Accounts
To list user accounts you can use `client.user.listAccounts` method. Which will return all accounts associated with a user.
```ts
const accounts = await authClient.listAccounts();
```
### Token Encryption
Better Auth doesn’t encrypt tokens by default and that’s intentional. We want you to have full control over how encryption and decryption are handled, rather than baking in behavior that could be confusing or limiting. If you need to store encrypted tokens (like accessToken or refreshToken), you can use databaseHooks to encrypt them before they’re saved to your database.
```ts
export const auth = betterAuth({
databaseHooks: {
account: {
create: {
before(account, context) {
const withEncryptedTokens = { ...account };
if (account.accessToken) {
const encryptedAccessToken = encrypt(account.accessToken) // [!code highlight]
withEncryptedTokens.accessToken = encryptedAccessToken;
}
if (account.refreshToken) {
const encryptedRefreshToken = encrypt(account.refreshToken); // [!code highlight]
withEncryptedTokens.refreshToken = encryptedRefreshToken;
}
return {
data: withEncryptedTokens
}
},
}
}
}
})
```
Then whenever you retrieve back the account make sure to decrypt the tokens before using them.
### Account Linking
Account linking enables users to associate multiple authentication methods with a single account. With Better Auth, users can connect additional social sign-ons or OAuth providers to their existing accounts if the provider confirms the user's email as verified.
If account linking is disabled, no accounts can be linked, regardless of the provider or email verification status.
```ts title="auth.ts"
export const auth = betterAuth({
account: {
accountLinking: {
enabled: true,
}
},
});
```
#### Forced Linking
You can specify a list of "trusted providers." When a user logs in using a trusted provider, their account will be automatically linked even if the provider doesn’t confirm the email verification status. Use this with caution as it may increase the risk of account takeover.
```ts title="auth.ts"
export const auth = betterAuth({
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"]
}
},
});
```
#### Manually Linking Accounts
Users already signed in can manually link their account to additional social providers or credential-based accounts.
* **Linking Social Accounts:** Use the `linkSocial` method on the client to link a social provider to the user's account.
```ts
await authClient.linkSocial({
provider: "google", // Provider to link
callbackURL: "/callback" // Callback URL after linking completes
});
```
You can also request specific scopes when linking a social account, which can be different from the scopes used during the initial authentication:
```ts
await authClient.linkSocial({
provider: "google",
callbackURL: "/callback",
scopes: ["https://www.googleapis.com/auth/drive.readonly"] // Request additional scopes
});
```
You can also link accounts using ID tokens directly, without redirecting to the provider's OAuth flow:
```ts
await authClient.linkSocial({
provider: "google",
idToken: {
token: "id_token_from_provider",
nonce: "nonce_used_for_token", // Optional
accessToken: "access_token", // Optional, may be required by some providers
refreshToken: "refresh_token" // Optional
}
});
```
This is useful when you already have valid tokens from the provider, for example:
* After signing in with a native SDK
* When using a mobile app that handles authentication
* When implementing custom OAuth flows
The ID token must be valid and the provider must support ID token verification.
If you want your users to be able to link a social account with a different email address than the user, or if you want to use a provider that does not return email addresses, you will need to enable this in the account linking settings.
```ts title="auth.ts"
export const auth = betterAuth({
account: {
accountLinking: {
allowDifferentEmails: true
}
},
});
```
If you want the newly linked accounts to update the user information, you need to enable this in the account linking settings.
```ts title="auth.ts"
export const auth = betterAuth({
account: {
accountLinking: {
updateUserInfoOnLink: true
}
},
});
```
* **Linking Credential-Based Accounts:** To link a credential-based account (e.g., email and password), users can initiate a "forgot password" flow, or you can call the `setPassword` method on the server.
```ts
await auth.api.setPassword({
headers: /* headers containing the user's session token */,
password: /* new password */
});
```
`setPassword` can't be called from the client for security reasons.
### Account Unlinking
You can unlink a user account by providing a `providerId`.
```ts
await authClient.unlinkAccount({
providerId: "google"
});
// Unlink a specific account
await authClient.unlinkAccount({
providerId: "google",
accountId: "123"
});
```
If the account doesn't exist, it will throw an error. Additionally, if the user only has one account, unlinking will be prevented to stop account lockout (unless `allowUnlinkingAll` is set to `true`).
```ts title="auth.ts"
export const auth = betterAuth({
account: {
accountLinking: {
allowUnlinkingAll: true
}
},
});
```
# guides: Migrating from Auth0 to Better Auth
URL: /docs/guides/auth0-migration-guide
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/auth0-migration-guide.mdx
A step-by-step guide to transitioning from Auth0 to Better Auth.
***
title: Migrating from Auth0 to Better Auth
description: A step-by-step guide to transitioning from Auth0 to Better Auth.
-----------------------------------------------------------------------------
In this guide, we'll walk through the steps to migrate a project from Auth0 to Better Auth — including email/password with proper hashing, social/external accounts, two-factor authentication, and more.
This migration will invalidate all active sessions. This guide doesn't currently show you how to migrate Organizations but it should be possible with additional steps and the [Organization](/docs/plugins/organization) Plugin.
## Before You Begin
Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started.
### Connect to your database
You'll need to connect to your database to migrate the users and accounts. You can use any database you want, but for this example, we'll use PostgreSQL.
npm
pnpm
yarn
bun
```bash
npm install pg
```
```bash
pnpm add pg
```
```bash
yarn add pg
```
```bash
bun add pg
```
And then you can use the following code to connect to your database.
```ts title="auth.ts"
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
})
```
### Enable Email and Password (Optional)
Enable the email and password in your auth config and implement your own logic for sending verification emails, reset password emails, etc.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: { // [!code highlight]
enabled: true, // [!code highlight]
}, // [!code highlight]
emailVerification: {
sendVerificationEmail: async({ user, url })=>{
// implement your logic here to send email verification
}
},
})
```
See [Email and Password](/docs/authentication/email-password) for more configuration options.
### Setup Social Providers (Optional)
Add social providers you have enabled in your Auth0 project in your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: { // [!code highlight]
google: { // [!code highlight]
clientId: process.env.GOOGLE_CLIENT_ID, // [!code highlight]
clientSecret: process.env.GOOGLE_CLIENT_SECRET, // [!code highlight]
}, // [!code highlight]
github: { // [!code highlight]
clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight]
clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight]
} // [!code highlight]
} // [!code highlight]
})
```
### Add Plugins (Optional)
You can add the following plugins to your auth config based on your needs.
[Admin](/docs/plugins/admin) Plugin will allow you to manage users, user impersonations and app level roles and permissions.
[Two Factor](/docs/plugins/2fa) Plugin will allow you to add two-factor authentication to your application.
[Username](/docs/plugins/username) Plugin will allow you to add username authentication to your application.
```ts title="auth.ts"
import { Pool } from "pg";
import { betterAuth } from "better-auth";
import { admin, twoFactor, username } from "better-auth/plugins";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
password: {
verify: (data) => {
// this for an edgecase that you might run in to on verifying the password
}
}
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
},
plugins: [admin(), twoFactor(), username()], // [!code highlight]
})
```
### Generate Schema
If you're using a custom database adapter, generate the schema:
```sh
npx @better-auth/cli generate
```
or if you're using the default adapter, you can use the following command:
```sh
npx @better-auth/cli migrate
```
### Install Dependencies
Install the required dependencies for the migration:
```bash
npm install auth0
```
### Create the migration script
Create a new file called `migrate-auth0.ts` in the `scripts` folder and add the following code:
Instead of using the Management API, you can use Auth0's bulk user export functionality and pass the exported JSON data directly to the `auth0Users` array. This is especially useful if you need to migrate password hashes and complete user data, which are not available through the Management API.
**Important Notes:**
* Password hashes export is only available for Auth0 Enterprise users
* Free plan users cannot export password hashes and will need to request a support ticket
* For detailed information about bulk user exports, see the [Auth0 Bulk User Export Documentation](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports)
* For password hash export details, refer to [Exporting Password Hashes](https://auth0.com/docs/troubleshoot/customer-support/manage-subscriptions/export-data#user-passwords)
Example:
```ts
// Replace this with your exported users JSON data
const auth0Users = [
{
"email": "helloworld@gmail.com",
"email_verified": false,
"name": "Hello world",
// Note: password_hash is only available for Enterprise users
"password_hash": "$2b$10$w4kfaZVjrcQ6ZOMiG.M8JeNvnVQkPKZV03pbDUHbxy9Ug0h/McDXi",
// ... other user data
}
];
```
```ts title="scripts/migrate-auth0.ts"
import { ManagementClient } from 'auth0';
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
import { auth } from '@/lib/auth';
const auth0Client = new ManagementClient({
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_SECRET!,
});
function safeDateConversion(timestamp?: string | number): Date {
if (!timestamp) return new Date();
const numericTimestamp = typeof timestamp === 'string' ? Date.parse(timestamp) : timestamp;
const milliseconds = numericTimestamp < 1000000000000 ? numericTimestamp * 1000 : numericTimestamp;
const date = new Date(milliseconds);
if (isNaN(date.getTime())) {
console.warn(`Invalid timestamp: ${timestamp}, falling back to current date`);
return new Date();
}
// Check for unreasonable dates (before 2000 or after 2100)
const year = date.getFullYear();
if (year < 2000 || year > 2100) {
console.warn(`Suspicious date year: ${year}, falling back to current date`);
return new Date();
}
return date;
}
// Helper function to generate backup codes for 2FA
async function generateBackupCodes(secret: string) {
const key = secret;
const backupCodes = Array.from({ length: 10 })
.fill(null)
.map(() => generateRandomString(10, "a-z", "0-9", "A-Z"))
.map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
const encCodes = await symmetricEncrypt({
data: JSON.stringify(backupCodes),
key: key,
});
return encCodes;
}
function mapAuth0RoleToBetterAuthRole(auth0Roles: string[]) {
if (typeof auth0Roles === 'string') return auth0Roles;
if (Array.isArray(auth0Roles)) return auth0Roles.join(',');
}
// helper function to migrate password from auth0 to better auth for custom hashes and algs
async function migratePassword(auth0User: any) {
if (auth0User.password_hash) {
if (auth0User.password_hash.startsWith('$2a$') || auth0User.password_hash.startsWith('$2b$')) {
return auth0User.password_hash;
}
}
if (auth0User.custom_password_hash) {
const customHash = auth0User.custom_password_hash;
if (customHash.algorithm === 'bcrypt') {
const hash = customHash.hash.value;
if (hash.startsWith('$2a$') || hash.startsWith('$2b$')) {
return hash;
}
}
return JSON.stringify({
algorithm: customHash.algorithm,
hash: {
value: customHash.hash.value,
encoding: customHash.hash.encoding || 'utf8',
...(customHash.hash.digest && { digest: customHash.hash.digest }),
...(customHash.hash.key && {
key: {
value: customHash.hash.key.value,
encoding: customHash.hash.key.encoding || 'utf8'
}
})
},
...(customHash.salt && {
salt: {
value: customHash.salt.value,
encoding: customHash.salt.encoding || 'utf8',
position: customHash.salt.position || 'prefix'
}
}),
...(customHash.password && {
password: {
encoding: customHash.password.encoding || 'utf8'
}
}),
...(customHash.algorithm === 'scrypt' && {
keylen: customHash.keylen,
cost: customHash.cost || 16384,
blockSize: customHash.blockSize || 8,
parallelization: customHash.parallelization || 1
})
});
}
return null;
}
async function migrateMFAFactors(auth0User: any, userId: string | undefined, ctx: any) {
if (!userId || !auth0User.mfa_factors || !Array.isArray(auth0User.mfa_factors)) {
return;
}
for (const factor of auth0User.mfa_factors) {
try {
if (factor.totp && factor.totp.secret) {
await ctx.adapter.create({
model: "twoFactor",
data: {
userId: userId,
secret: factor.totp.secret,
backupCodes: await generateBackupCodes(factor.totp.secret)
}
});
}
} catch (error) {
console.error(`Failed to migrate MFA factor for user ${userId}:`, error);
}
}
}
async function migrateOAuthAccounts(auth0User: any, userId: string | undefined, ctx: any) {
if (!userId || !auth0User.identities || !Array.isArray(auth0User.identities)) {
return;
}
for (const identity of auth0User.identities) {
try {
const providerId = identity.provider === 'auth0' ? "credential" : identity.provider.split("-")[0];
await ctx.adapter.create({
model: "account",
data: {
id: `${auth0User.user_id}|${identity.provider}|${identity.user_id}`,
userId: userId,
password: await migratePassword(auth0User),
providerId: providerId || identity.provider,
accountId: identity.user_id,
accessToken: identity.access_token,
tokenType: identity.token_type,
refreshToken: identity.refresh_token,
accessTokenExpiresAt: identity.expires_in ? new Date(Date.now() + identity.expires_in * 1000) : undefined,
// if you are enterprise user, you can get the refresh tokens or all the tokensets - auth0Client.users.getAllTokensets
refreshTokenExpiresAt: identity.refresh_token_expires_in ? new Date(Date.now() + identity.refresh_token_expires_in * 1000) : undefined,
scope: identity.scope,
idToken: identity.id_token,
createdAt: safeDateConversion(auth0User.created_at),
updatedAt: safeDateConversion(auth0User.updated_at)
},
forceAllowId: true
}).catch((error: Error) => {
console.error(`Failed to create OAuth account for user ${userId} with provider ${providerId}:`, error);
return ctx.adapter.create({
// Try creating without optional fields if the first attempt failed
model: "account",
data: {
id: `${auth0User.user_id}|${identity.provider}|${identity.user_id}`,
userId: userId,
password: migratePassword(auth0User),
providerId: providerId,
accountId: identity.user_id,
accessToken: identity.access_token,
tokenType: identity.token_type,
refreshToken: identity.refresh_token,
accessTokenExpiresAt: identity.expires_in ? new Date(Date.now() + identity.expires_in * 1000) : undefined,
refreshTokenExpiresAt: identity.refresh_token_expires_in ? new Date(Date.now() + identity.refresh_token_expires_in * 1000) : undefined,
scope: identity.scope,
idToken: identity.id_token,
createdAt: safeDateConversion(auth0User.created_at),
updatedAt: safeDateConversion(auth0User.updated_at)
},
forceAllowId: true
});
});
console.log(`Successfully migrated OAuth account for user ${userId} with provider ${providerId}`);
} catch (error) {
console.error(`Failed to migrate OAuth account for user ${userId}:`, error);
}
}
}
async function migrateOrganizations(ctx: any) {
try {
const organizations = await auth0Client.organizations.getAll();
for (const org of organizations.data || []) {
try {
await ctx.adapter.create({
model: "organization",
data: {
id: org.id,
name: org.display_name || org.id,
slug: (org.display_name || org.id).toLowerCase().replace(/[^a-z0-9]/g, '-'),
logo: org.branding?.logo_url,
metadata: JSON.stringify(org.metadata || {}),
createdAt: safeDateConversion(org.created_at),
},
forceAllowId: true
});
const members = await auth0Client.organizations.getMembers({ id: org.id });
for (const member of members.data || []) {
try {
const userRoles = await auth0Client.organizations.getMemberRoles({
id: org.id,
user_id: member.user_id
});
const role = mapAuth0RoleToBetterAuthRole(userRoles.data?.map(r => r.name) || []);
await ctx.adapter.create({
model: "member",
data: {
id: `${org.id}|${member.user_id}`,
organizationId: org.id,
userId: member.user_id,
role: role,
createdAt: new Date()
},
forceAllowId: true
});
console.log(`Successfully migrated member ${member.user_id} for organization ${org.display_name || org.id}`);
} catch (error) {
console.error(`Failed to migrate member ${member.user_id} for organization ${org.display_name || org.id}:`, error);
}
}
console.log(`Successfully migrated organization: ${org.display_name || org.id}`);
} catch (error) {
console.error(`Failed to migrate organization ${org.display_name || org.id}:`, error);
}
}
console.log('Organization migration completed');
} catch (error) {
console.error('Failed to migrate organizations:', error);
}
}
async function migrateFromAuth0() {
try {
const ctx = await auth.$context;
const isAdminEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "admin");
const isUsernameEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "username");
const isOrganizationEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "organization");
const perPage = 100;
const auth0Users: any[] = [];
let pageNumber = 0;
while (true) {
try {
const params = {
per_page: perPage,
page: pageNumber,
include_totals: true,
};
const response = (await auth0Client.users.getAll(params)).data as any;
const users = response.users || [];
if (users.length === 0) break;
auth0Users.push(...users);
pageNumber++;
if (users.length < perPage) break;
} catch (error) {
console.error('Error fetching users:', error);
break;
}
}
console.log(`Found ${auth0Users.length} users to migrate`);
for (const auth0User of auth0Users) {
try {
// Determine if this is a password-based or OAuth user
const isOAuthUser = auth0User.identities?.some((identity: any) => identity.provider !== 'auth0');
// Base user data that's common for both types
const baseUserData = {
id: auth0User.user_id,
email: auth0User.email,
emailVerified: auth0User.email_verified || false,
name: auth0User.name || auth0User.nickname,
image: auth0User.picture,
createdAt: safeDateConversion(auth0User.created_at),
updatedAt: safeDateConversion(auth0User.updated_at),
...(isAdminEnabled ? {
banned: auth0User.blocked || false,
role: mapAuth0RoleToBetterAuthRole(auth0User.roles || []),
} : {}),
...(isUsernameEnabled ? {
username: auth0User.username || auth0User.nickname,
} : {}),
};
const createdUser = await ctx.adapter.create({
model: "user",
data: {
...baseUserData,
},
forceAllowId: true
});
if (!createdUser?.id) {
throw new Error('Failed to create user');
}
await migrateOAuthAccounts(auth0User, createdUser.id, ctx)
console.log(`Successfully migrated user: ${auth0User.email}`);
} catch (error) {
console.error(`Failed to migrate user ${auth0User.email}:`, error);
}
}
if (isOrganizationEnabled) {
await migrateOrganizations(ctx);
}
// the reset of migration will be here.
console.log('Migration completed successfully');
} catch (error) {
console.error('Migration failed:', error);
throw error;
}
}
migrateFromAuth0()
.then(() => {
console.log('Migration completed');
process.exit(0);
})
.catch((error) => {
console.error('Migration failed:', error);
process.exit(1);
});
```
Make sure to replace the Auth0 environment variables with your own values:
* `AUTH0_DOMAIN`
* `AUTH0_CLIENT_ID`
* `AUTH0_SECRET`
### Run the migration
Run the migration script:
```sh
bun run scripts/migrate-auth0.ts # or use your preferred runtime
```
Important considerations:
1. Test the migration in a development environment first
2. Monitor the migration process for any errors
3. Verify the migrated data in Better Auth before proceeding
4. Keep Auth0 installed and configured until the migration is complete
5. The script handles bcrypt password hashes by default. For custom password hashing algorithms, you'll need to modify the `migratePassword` function
### Verify the migration
After running the migration, verify that:
1. All users have been properly migrated
2. Social connections are working
3. Password-based authentication is working
4. Two-factor authentication settings are preserved (if enabled)
5. User roles and permissions are correctly mapped
### Update your components
Now that the data is migrated, update your components to use Better Auth. Here's an example for the sign-in component:
```tsx title="components/auth/sign-in.tsx"
import { authClient } from "better-auth/client";
export const SignIn = () => {
const handleSignIn = async () => {
const { data, error } = await authClient.signIn.email({
email: "helloworld@gmail.com",
password: "helloworld",
});
if (error) {
console.error(error);
return;
}
// Handle successful sign in
};
return (
);
};
```
### Update the middleware
Replace your Auth0 middleware with Better Auth's middleware:
```ts title="middleware.ts"
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
const { pathname } = request.nextUrl;
if (sessionCookie && ["/login", "/signup"].includes(pathname)) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
if (!sessionCookie && pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard", "/login", "/signup"],
};
```
### Remove Auth0 Dependencies
Once you've verified everything is working correctly with Better Auth, remove Auth0:
```bash
npm remove @auth0/auth0-react @auth0/auth0-spa-js @auth0/nextjs-auth0
```
## Additional Considerations
### Password Migration
The migration script handles bcrypt password hashes by default. If you're using custom password hashing algorithms in Auth0, you'll need to modify the `migratePassword` function in the migration script to handle your specific case.
### Role Mapping
The script includes a basic role mapping function (`mapAuth0RoleToBetterAuthRole`). Customize this function based on your Auth0 roles and Better Auth role requirements.
### Rate Limiting
The migration script includes pagination to handle large numbers of users. Adjust the `perPage` value based on your needs and Auth0's rate limits.
## Wrapping Up
Now! You've successfully migrated from Auth0 to Better Auth.
Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential.
# guides: Browser Extension Guide
URL: /docs/guides/browser-extension-guide
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/browser-extension-guide.mdx
A step-by-step guide to creating a browser extension with Better Auth.
***
title: Browser Extension Guide
description: A step-by-step guide to creating a browser extension with Better Auth.
-----------------------------------------------------------------------------------
In this guide, we'll walk you through the steps of creating a browser extension using Plasmo with Better Auth for authentication.
If you would like to view a completed example, you can check out the browser extension example.
The Plasmo framework does not provide a backend for the browser extension.
This guide assumes you have{" "}
a backend setup of Better Auth and
are ready to create a browser extension to connect to it.
## Setup & Installations
Initialize a new Plasmo project with TailwindCSS and a src directory.
```bash
pnpm create plasmo --with-tailwindcss --with-src
```
Then, install the Better Auth package.
```bash
pnpm add better-auth
```
To start the Plasmo development server, run the following command.
```bash
pnpm dev
```
## Configure tsconfig
Configure the `tsconfig.json` file to include `strict` mode.
For this demo, we have also changed the import alias from `~` to `@` and set it to the `src` directory.
```json title="tsconfig.json"
{
"compilerOptions": {
"paths": {
"@/_": [
"./src/_"
]
},
"strict": true,
"baseUrl": "."
}
}
```
## Create the client auth instance
Create a new file at `src/auth/auth-client.ts` and add the following code.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" /* Base URL of your Better Auth backend. */,
plugins: [],
});
```
## Configure the manifest
We must ensure the extension knows the URL to the Better Auth backend.
Head to your package.json file, and add the following code.
```json title="package.json"
{
//...
"manifest": {
"host_permissions": [
"https://URL_TO_YOUR_BACKEND" // localhost works too (e.g. http://localhost:3000)
]
}
}
```
## You're now ready!
You have now set up Better Auth for your browser extension.
Add your desired UI and create your dream extension!
To learn more about the client Better Auth API, check out the client documentation.
Here's a quick example 😎
```tsx title="src/popup.tsx"
import { authClient } from "./auth/auth-client"
function IndexPopup() {
const {data, isPending, error} = authClient.useSession();
if(isPending){
return <>Loading...>
}
if(error){
return <>Error: {error.message}>
}
if(data){
return <>Signed in as {data.user.name}>
}
}
export default IndexPopup;
```
## Bundle your extension
To get a production build, run the following command.
```bash
pnpm build
```
Head over to chrome://extensions and enable developer mode.
Click on "Load Unpacked" and navigate to your extension's `build/chrome-mv3-dev` (or `build/chrome-mv3-prod`) directory.
To see your popup, click on the puzzle piece icon on the Chrome toolbar, and click on your extension.
Learn more about bundling your extension here.
## Configure the server auth instance
First, we will need your extension URL.
An extension URL formed like this: `chrome-extension://YOUR_EXTENSION_ID`.
You can find your extension ID at chrome://extensions.
Head to your server's auth file, and make sure that your extension's URL is added to the `trustedOrigins` list.
```ts title="server.ts"
import { betterAuth } from "better-auth"
import { auth } from "@/auth/auth"
export const auth = betterAuth({
trustedOrigins: ["chrome-extension://YOUR_EXTENSION_ID"],
})
```
If you're developing multiple extensions or need to support different browser extensions with different IDs, you can use wildcard patterns:
```ts title="server.ts"
export const auth = betterAuth({
trustedOrigins: [
// Support a specific extension ID
"chrome-extension://YOUR_EXTENSION_ID",
// Or support multiple extensions with wildcard (less secure)
"chrome-extension://*"
],
})
```
Using wildcards for extension origins (`chrome-extension://*`) reduces security by trusting all extensions.
It's safer to explicitly list each extension ID you trust. Only use wildcards for development and testing.
## That's it!
Everything is set up! You can now start developing your extension. 🎉
## Wrapping Up
Congratulations! You've successfully created a browser extension using Better Auth and Plasmo.
We highly recommend you visit the Plasmo documentation to learn more about the framework.
If you would like to view a completed example, you can check out the browser extension example.
If you have any questions, feel free to open an issue on our GitHub repo, or join our Discord server for support.
# guides: Migrating from Clerk to Better Auth
URL: /docs/guides/clerk-migration-guide
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/clerk-migration-guide.mdx
A step-by-step guide to transitioning from Clerk to Better Auth.
***
title: Migrating from Clerk to Better Auth
description: A step-by-step guide to transitioning from Clerk to Better Auth.
-----------------------------------------------------------------------------
In this guide, we'll walk through the steps to migrate a project from Clerk to Better Auth — including email/password with proper hashing, social/external accounts, phone number, two-factor data, and more.
This migration will invalidate all active sessions. This guide doesn't currently show you how to migrate Organization but it should be possible with additional steps and the [Organization](/docs/plugins/organization) Plugin.
## Before You Begin
Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started. And go to
### Connect to your database
You'll need to connect to your database to migrate the users and accounts. You can use any database you want, but for this example, we'll use PostgreSQL.
npm
pnpm
yarn
bun
```bash
npm install pg
```
```bash
pnpm add pg
```
```bash
yarn add pg
```
```bash
bun add pg
```
And then you can use the following code to connect to your database.
```ts title="auth.ts"
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
})
```
### Enable Email and Password (Optional)
Enable the email and password in your auth config and implement your own logic for sending verification emails, reset password emails, etc.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: { // [!code highlight]
enabled: true, // [!code highlight]
}, // [!code highlight]
emailVerification: {
sendVerificationEmail: async({ user, url })=>{
// implement your logic here to send email verification
}
},
})
```
See [Email and Password](/docs/authentication/email-password) for more configuration options.
### Setup Social Providers (Optional)
Add social providers you have enabled in your Clerk project in your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: { // [!code highlight]
github: { // [!code highlight]
clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight]
clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight]
} // [!code highlight]
} // [!code highlight]
})
```
### Add Plugins (Optional)
You can add the following plugins to your auth config based on your needs.
[Admin](/docs/plugins/admin) Plugin will allow you to manage users, user impersonations and app level roles and permissions.
[Two Factor](/docs/plugins/2fa) Plugin will allow you to add two-factor authentication to your application.
[Phone Number](/docs/plugins/phone-number) Plugin will allow you to add phone number authentication to your application.
[Username](/docs/plugins/username) Plugin will allow you to add username authentication to your application.
```ts title="auth.ts"
import { Pool } from "pg";
import { betterAuth } from "better-auth";
import { admin, twoFactor, phoneNumber, username } from "better-auth/plugins";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
},
plugins: [admin(), twoFactor(), phoneNumber(), username()], // [!code highlight]
})
```
### Generate Schema
If you're using a custom database adapter, generate the schema:
```sh
npx @better-auth/cli generate
```
or if you're using the default adapter, you can use the following command:
```sh
npx @better-auth/cli migrate
```
### Export Clerk Users
Go to the Clerk dashboard and export the users. Check how to do it [here](https://clerk.com/docs/deployments/exporting-users#export-your-users-data-from-the-clerk-dashboard). It will download a CSV file with the users data. You need to save it as `exported_users.csv` and put it in the root of your project.
### Create the migration script
Create a new file called `migrate-clerk.ts` in the `scripts` folder and add the following code:
```ts title="scripts/migrate-clerk.ts"
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
import { auth } from "@/lib/auth"; // import your auth instance
function getCSVData(csv: string) {
const lines = csv.split('\n').filter(line => line.trim());
const headers = lines[0]?.split(',').map(header => header.trim()) || [];
const jsonData = lines.slice(1).map(line => {
const values = line.split(',').map(value => value.trim());
return headers.reduce((obj, header, index) => {
obj[header] = values[index] || '';
return obj;
}, {} as Record);
});
return jsonData as Array<{
id: string;
first_name: string;
last_name: string;
username: string;
primary_email_address: string;
primary_phone_number: string;
verified_email_addresses: string;
unverified_email_addresses: string;
verified_phone_numbers: string;
unverified_phone_numbers: string;
totp_secret: string;
password_digest: string;
password_hasher: string;
}>;
}
const exportedUserCSV = await Bun.file("exported_users.csv").text(); // this is the file you downloaded from Clerk
async function getClerkUsers(totalUsers: number) {
const clerkUsers: {
id: string;
first_name: string;
last_name: string;
username: string;
image_url: string;
password_enabled: boolean;
two_factor_enabled: boolean;
totp_enabled: boolean;
backup_code_enabled: boolean;
banned: boolean;
locked: boolean;
lockout_expires_in_seconds: number;
created_at: number;
updated_at: number;
external_accounts: {
id: string;
provider: string;
identification_id: string;
provider_user_id: string;
approved_scopes: string;
email_address: string;
first_name: string;
last_name: string;
image_url: string;
created_at: number;
updated_at: number;
}[]
}[] = [];
for (let i = 0; i < totalUsers; i += 500) {
const response = await fetch(`https://api.clerk.com/v1/users?offset=${i}&limit=${500}`, {
headers: {
'Authorization': `Bearer ${process.env.CLERK_SECRET_KEY}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch users: ${response.statusText}`);
}
const clerkUsersData = await response.json();
// biome-ignore lint/suspicious/noExplicitAny:
clerkUsers.push(...clerkUsersData as any);
}
return clerkUsers;
}
export async function generateBackupCodes(
secret: string,
) {
const key = secret;
const backupCodes = Array.from({ length: 10 })
.fill(null)
.map(() => generateRandomString(10, "a-z", "0-9", "A-Z"))
.map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
const encCodes = await symmetricEncrypt({
data: JSON.stringify(backupCodes),
key: key,
});
return encCodes
}
// Helper function to safely convert timestamp to Date
function safeDateConversion(timestamp?: number): Date {
if (!timestamp) return new Date();
// Convert seconds to milliseconds
const date = new Date(timestamp * 1000);
// Check if the date is valid
if (isNaN(date.getTime())) {
console.warn(`Invalid timestamp: ${timestamp}, falling back to current date`);
return new Date();
}
// Check for unreasonable dates (before 2000 or after 2100)
const year = date.getFullYear();
if (year < 2000 || year > 2100) {
console.warn(`Suspicious date year: ${year}, falling back to current date`);
return new Date();
}
return date;
}
async function migrateFromClerk() {
const jsonData = getCSVData(exportedUserCSV);
const clerkUsers = await getClerkUsers(jsonData.length);
const ctx = await auth.$context
const isAdminEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "admin");
const isTwoFactorEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "two-factor");
const isUsernameEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "username");
const isPhoneNumberEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "phone-number");
for (const user of jsonData) {
const { id, first_name, last_name, username, primary_email_address, primary_phone_number, verified_email_addresses, unverified_email_addresses, verified_phone_numbers, unverified_phone_numbers, totp_secret, password_digest, password_hasher } = user;
const clerkUser = clerkUsers.find(clerkUser => clerkUser?.id === id);
// create user
const createdUser = await ctx.adapter.create<{
id: string;
}>({
model: "user",
data: {
id,
email: primary_email_address,
emailVerified: verified_email_addresses.length > 0,
name: `${first_name} ${last_name}`,
image: clerkUser?.image_url,
createdAt: safeDateConversion(clerkUser?.created_at),
updatedAt: safeDateConversion(clerkUser?.updated_at),
// # Two Factor (if you enabled two factor plugin)
...(isTwoFactorEnabled ? {
twoFactorEnabled: clerkUser?.two_factor_enabled
} : {}),
// # Admin (if you enabled admin plugin)
...(isAdminEnabled ? {
banned: clerkUser?.banned,
banExpiresAt: clerkUser?.lockout_expires_in_seconds,
role: "user"
} : {}),
// # Username (if you enabled username plugin)
...(isUsernameEnabled ? {
username: username,
} : {}),
// # Phone Number (if you enabled phone number plugin)
...(isPhoneNumberEnabled ? {
phoneNumber: primary_phone_number,
phoneNumberVerified: verified_phone_numbers.length > 0,
} : {}),
},
forceAllowId: true
}).catch(async e => {
return await ctx.adapter.findOne<{
id: string;
}>({
model: "user",
where: [{
field: "id",
value: id
}]
})
})
// create external account
const externalAccounts = clerkUser?.external_accounts;
if (externalAccounts) {
for (const externalAccount of externalAccounts) {
const { id, provider, identification_id, provider_user_id, approved_scopes, email_address, first_name, last_name, image_url, created_at, updated_at } = externalAccount;
if (externalAccount.provider === "credential") {
await ctx.adapter.create({
model: "account",
data: {
id,
providerId: provider,
accountId: externalAccount.provider_user_id,
scope: approved_scopes,
userId: createdUser?.id,
createdAt: safeDateConversion(created_at),
updatedAt: safeDateConversion(updated_at),
password: password_digest,
}
})
} else {
await ctx.adapter.create({
model: "account",
data: {
id,
providerId: provider.replace("oauth_", ""),
accountId: externalAccount.provider_user_id,
scope: approved_scopes,
userId: createdUser?.id,
createdAt: safeDateConversion(created_at),
updatedAt: safeDateConversion(updated_at),
},
forceAllowId: true
})
}
}
}
//two factor
if (isTwoFactorEnabled) {
await ctx.adapter.create({
model: "twoFactor",
data: {
userId: createdUser?.id,
secret: totp_secret,
backupCodes: await generateBackupCodes(totp_secret)
}
})
}
}
}
migrateFromClerk()
.then(() => {
console.log('Migration completed');
process.exit(0);
})
.catch((error) => {
console.error('Migration failed:', error);
process.exit(1);
});
```
Make sure to replace the `process.env.CLERK_SECRET_KEY` with your own Clerk secret key. Feel free to customize the script to your needs.
### Run the migration
Run the migration:
```sh
bun run script/migrate-clerk.ts # you can use any thing you like to run the script
```
Make sure to:
1. Test the migration in a development environment first
2. Monitor the migration process for any errors
3. Verify the migrated data in Better Auth before proceeding
4. Keep Clerk installed and configured until the migration is complete
### Verify the migration
After running the migration, verify that all users have been properly migrated by checking the database.
### Update your components
Now that the data is migrated, you can start updating your components to use Better Auth. Here's an example for the sign-in component:
```tsx title="components/auth/sign-in.tsx"
import { authClient } from "better-auth/client";
export const SignIn = () => {
const handleSignIn = async () => {
const { data, error } = await authClient.signIn.email({
email: "user@example.com",
password: "password",
});
if (error) {
console.error(error);
return;
}
// Handle successful sign in
};
return (
);
};
```
### Update the middleware
Replace your Clerk middleware with Better Auth's middleware:
```ts title="middleware.ts"
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
const { pathname } = request.nextUrl;
if (sessionCookie && ["/login", "/signup"].includes(pathname)) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
if (!sessionCookie && pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard", "/login", "/signup"],
};
```
### Remove Clerk Dependencies
Once you've verified that everything is working correctly with Better Auth, you can remove Clerk:
```bash title="Remove Clerk"
pnpm remove @clerk/nextjs @clerk/themes @clerk/types
```
## Additional Resources
[Goodbye Clerk, Hello Better Auth – Full Migration Guide!](https://www.youtube.com/watch?v=Za_QihbDSuk)
## Wrapping Up
Congratulations! You've successfully migrated from Clerk to Better Auth.
Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential.
# guides: Create a Database Adapter
URL: /docs/guides/create-a-db-adapter
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/create-a-db-adapter.mdx
Learn how to create a custom database adapter for Better-Auth
***
title: Create a Database Adapter
description: Learn how to create a custom database adapter for Better-Auth
--------------------------------------------------------------------------
Learn how to create a custom database adapter for Better-Auth using `createAdapter`.
Our `createAdapter` function is designed to be very flexible, and we've done our best to make it easy to understand and use.
Our hope is to allow you to focus on writing database logic, and not have to worry about how the adapter is working with Better-Auth.
Anything from custom schema configurations, custom ID generation, safe JSON parsing, and more is handled by the `createAdapter` function.
All you need to do is provide the database logic, and the `createAdapter` function will handle the rest.
## Quick Start
### Get things ready
1. Import `createAdapter`.
2. Create `CustomAdapterConfig` interface that represents your adapter config options.
3. Create the adapter!
```ts
import { createAdapter, type AdapterDebugLogs } from "better-auth/adapters";
// Your custom adapter config options
interface CustomAdapterConfig {
/**
* Helps you debug issues with the adapter.
*/
debugLogs?: AdapterDebugLogs;
/**
* If the table names in the schema are plural.
*/
usePlural?: boolean;
}
export const myAdapter = (config: CustomAdapterConfig = {}) =>
createAdapter({
// ...
});
```
### Configure the adapter
The `config` object is mostly used to provide information about the adapter to Better-Auth.
We try to minimize the amount of code you need to write in your adapter functions, and these `config` options are used to help us do that.
```ts
// ...
export const myAdapter = (config: CustomAdapterConfig = {}) =>
createAdapter({
config: {
adapterId: "custom-adapter", // A unique identifier for the adapter.
adapterName: "Custom Adapter", // The name of the adapter.
usePlural: config.usePlural ?? false, // Whether the table names in the schema are plural.
debugLogs: config.debugLogs ?? false, // Whether to enable debug logs.
supportsJSON: false, // Whether the database supports JSON. (Default: false)
supportsDates: true, // Whether the database supports dates. (Default: true)
supportsBooleans: true, // Whether the database supports booleans. (Default: true)
supportsNumericIds: true, // Whether the database supports auto-incrementing numeric IDs. (Default: true)
},
// ...
});
```
### Create the adapter
The `adapter` function is where you write the code that interacts with your database.
```ts
// ...
export const myAdapter = (config: CustomAdapterConfig = {}) =>
createAdapter({
config: {
// ...
},
adapter: ({}) => {
return {
create: async ({ data, model, select }) => {
// ...
},
update: async ({ data, model, select }) => {
// ...
},
updateMany: async ({ data, model, select }) => {
// ...
},
delete: async ({ data, model, select }) => {
// ...
},
// ...
};
},
});
```
Learn more about the `adapter` here [here](/docs/concepts/database#adapters).
## Adapter
The `adapter` function is where you write the code that interacts with your database.
If you haven't already, check out the `options` object in the [config section](#config), as it can be useful for your adapter.
Before we get into the adapter function, let's go over the parameters that are available to you.
* `options`: The Better Auth options.
* `schema`: The schema from the user's Better Auth instance.
* `debugLog`: The debug log function.
* `getField`: The get field function.
* `getDefaultModelName`: The get default model name function.
* `getDefaultFieldName`: The get default field name function.
* `getFieldAttributes`: The get field attributes function.
```ts title="Example"
adapter: ({
options,
schema,
debugLog,
getField,
getDefaultModelName,
getDefaultFieldName,
}) => {
return {
// ...
};
};
```
### Adapter Methods
* All `model` values are already transformed into the correct model name for the database based on the end-user's schema configuration.
* This also means that if you need access to the `schema` version of a given model, you can't use this exact `model` value, you'll need to use the `getDefaultModelName` function provided in the options to convert the `model` to the `schema` version.
* We will automatically fill in any missing fields you return based on the user's `schema` configuration.
* Any method that includes a `select` parameter, is only for the purpose of getting data from your database more efficiently. You do not need to worry about only returning what the `select` parameter states, as we will handle that for you.
### `create` method
The `create` method is used to create a new record in the database.
Note:
If the user has enabled the `useNumberId` option, or if `generateId` is `false` in the user's Better Auth config,
then it's expected that the `id` is provided in the `data` object. Otherwise, the `id` will be automatically generated.
Additionally, it's possible to pass `forceAllowId` as a parameter to the `create` method, which allows `id` to be provided in the `data` object.
We handle `forceAllowId` internally, so you don't need to worry about it.
parameters:
* `model`: The model/table name that new data will be inserted into.
* `data`: The data to insert into the database.
* `select`: An array of fields to return from the database.
Make sure to return the data that is inserted into the database.
```ts title="Example"
create: async ({ model, data, select }) => {
// Example of inserting data into the database.
return await db.insert(model).values(data);
};
```
### `update` method
The `update` method is used to update a record in the database.
parameters:
* `model`: The model/table name that the record will be updated in.
* `where`: The `where` clause to update the record by.
* `update`: The data to update the record with.
Make sure to return the data in the row which is updated. This includes any
fields that were not updated.
```ts title="Example"
update: async ({ model, where, update }) => {
// Example of updating data in the database.
return await db.update(model).set(update).where(where);
};
```
### `updateMany` method
The `updateMany` method is used to update multiple records in the database.
parameters:
* `model`: The model/table name that the records will be updated in.
* `where`: The `where` clause to update the records by.
* `update`: The data to update the records with.
Make sure to return the number of records that were updated.
```ts title="Example"
updateMany: async ({ model, where, update }) => {
// Example of updating multiple records in the database.
return await db.update(model).set(update).where(where);
};
```
### `delete` method
The `delete` method is used to delete a record from the database.
parameters:
* `model`: The model/table name that the record will be deleted from.
* `where`: The `where` clause to delete the record by.
```ts title="Example"
delete: async ({ model, where }) => {
// Example of deleting a record from the database.
await db.delete(model).where(where);
}
```
### `deleteMany` method
The `deleteMany` method is used to delete multiple records from the database.
parameters:
* `model`: The model/table name that the records will be deleted from.
* `where`: The `where` clause to delete the records by.
Make sure to return the number of records that were deleted.
```ts title="Example"
deleteMany: async ({ model, where }) => {
// Example of deleting multiple records from the database.
return await db.delete(model).where(where);
};
```
### `findOne` method
The `findOne` method is used to find a single record in the database.
parameters:
* `model`: The model/table name that the record will be found in.
* `where`: The `where` clause to find the record by.
* `select`: The `select` clause to return.
Make sure to return the data that is found in the database.
```ts title="Example"
findOne: async ({ model, where, select }) => {
// Example of finding a single record in the database.
return await db.select().from(model).where(where).limit(1);
};
```
### `findMany` method
The `findMany` method is used to find multiple records in the database.
parameters:
* `model`: The model/table name that the records will be found in.
* `where`: The `where` clause to find the records by.
* `limit`: The limit of records to return.
* `sortBy`: The `sortBy` clause to sort the records by.
* `offset`: The offset of records to return.
Make sure to return the array of data that is found in the database.
```ts title="Example"
findMany: async ({ model, where, limit, sortBy, offset }) => {
// Example of finding multiple records in the database.
return await db
.select()
.from(model)
.where(where)
.limit(limit)
.offset(offset)
.orderBy(sortBy);
};
```
### `count` method
The `count` method is used to count the number of records in the database.
parameters:
* `model`: The model/table name that the records will be counted in.
* `where`: The `where` clause to count the records by.
Make sure to return the number of records that were counted.
```ts title="Example"
count: async ({ model, where }) => {
// Example of counting the number of records in the database.
return await db.select().from(model).where(where).count();
};
```
### `options` (optional)
The `options` object is for any potential config that you got from your custom adapter options.
```ts title="Example"
const myAdapter = (config: CustomAdapterConfig) =>
createAdapter({
config: {
// ...
},
adapter: ({ options }) => {
return {
options: config,
};
},
});
```
### `createSchema` (optional)
The `createSchema` method allows the [Better Auth CLI](/docs/concepts/cli) to [generate](/docs/concepts/cli/#generate) a schema for the database.
parameters:
* `tables`: The tables from the user's Better-Auth instance schema; which is expected to be generated into the schema file.
* `file`: The file the user may have passed in to the `generate` command as the expected schema file output path.
```ts title="Example"
createSchema: async ({ file, tables }) => {
// ... Custom logic to create a schema for the database.
};
```
## Test your adapter
We've provided a test suite that you can use to test your adapter. It requires you to use `vitest`.
```ts title="my-adapter.test.ts"
import { expect, test, describe } from "vitest";
import { runAdapterTest } from "better-auth/adapters/test";
import { myAdapter } from "./my-adapter";
describe("My Adapter Tests", async () => {
afterAll(async () => {
// Run DB cleanup here...
});
const adapter = myAdapter({
debugLogs: {
// If your adapter config allows passing in debug logs, then pass this here.
isRunningAdapterTests: true, // This is our super secret flag to let us know to only log debug logs if a test fails.
},
});
await runAdapterTest({
getAdapter: async (betterAuthOptions = {}) => {
return adapter(betterAuthOptions);
},
});
});
```
### Numeric ID tests
If your database supports numeric IDs, then you should run this test as well:
```ts title="my-adapter.number-id.test.ts"
import { expect, test, describe } from "vitest";
import { runNumberIdAdapterTest } from "better-auth/adapters/test";
import { myAdapter } from "./my-adapter";
describe("My Adapter Numeric ID Tests", async () => {
afterAll(async () => {
// Run DB cleanup here...
});
const adapter = myAdapter({
debugLogs: {
// If your adapter config allows passing in debug logs, then pass this here.
isRunningAdapterTests: true, // This is our super secret flag to let us know to only log debug logs if a test fails.
},
});
await runNumberIdAdapterTest({
getAdapter: async (betterAuthOptions = {}) => {
return adapter(betterAuthOptions);
},
});
});
```
## Config
The `config` object is used to provide information about the adapter to Better-Auth.
We **highly recommend** going through and reading each provided option below, as it will help you understand how to properly configure your adapter.
### Required Config
### `adapterId`
A unique identifier for the adapter.
### `adapterName`
The name of the adapter.
### Optional Config
### `supportsNumericIds`
Whether the database supports numeric IDs. If this is set to `false` and the user's config has enabled `useNumberId`, then we will throw an error.
### `supportsJSON`
Whether the database supports JSON. If the database doesn't support JSON, we will use a `string` to save the JSON data.And when we retrieve the data, we will safely parse the `string` back into a JSON object.
### `supportsDates`
Whether the database supports dates. If the database doesn't support dates, we will use a `string` to save the date. (ISO string) When we retrieve the data, we will safely parse the `string` back into a `Date` object.
### `supportsBooleans`
Whether the database supports booleans. If the database doesn't support booleans, we will use a `0` or `1` to save the boolean value. When we retrieve the data, we will safely parse the `0` or `1` back into a boolean value.
### `usePlural`
Whether the table names in the schema are plural. This is often defined by the user, and passed down through your custom adapter options. If you do not intend to allow the user to customize the table names, you can ignore this option, or set this to `false`.
```ts title="Example"
const adapter = myAdapter({
// This value then gets passed into the `usePlural`
// option in the createAdapter `config` object.
usePlural: true,
});
```
### `transaction`
Whether the adapter supports transactions. If `false`, operations run sequentially; otherwise provide a function that executes a callback with a `TransactionAdapter`.
If your database does not support transactions, the error handling and rollback
will not be as robust. We recommend using a database that supports transactions
for better data integrity.
### `debugLogs`
Used to enable debug logs for the adapter. You can pass in a boolean, or an object with the following keys: `create`, `update`, `updateMany`, `findOne`, `findMany`, `delete`, `deleteMany`, `count`.
If any of the keys are `true`, the debug logs will be enabled for that method.
```ts title="Example"
// Will log debug logs for all methods.
const adapter = myAdapter({
debugLogs: true,
});
```
```ts title="Example"
// Will only log debug logs for the `create` and `update` methods.
const adapter = myAdapter({
debugLogs: {
create: true,
update: true,
},
});
```
### `disableIdGeneration`
Whether to disable ID generation. If this is set to `true`, then the user's `generateId` option will be ignored.
### `customIdGenerator`
If your database only supports a specific custom ID generation, then you can use this option to generate your own IDs.
### `mapKeysTransformInput`
If your database uses a different key name for a given situation, you can use this option to map the keys. This is useful for databases that expect a different key name for a given situation.
For example, MongoDB uses `_id` while in Better-Auth we use `id`.
Each key in the returned object represents the old key to replace.
The value represents the new key.
This can be a partial object that only transforms some keys.
```ts title="Example"
mapKeysTransformInput: () => {
return {
id: "_id", // We want to replace `id` to `_id` to save into MongoDB
};
},
```
### `mapKeysTransformOutput`
If your database uses a different key name for a given situation, you can use this option to map the keys. This is useful for databases that use a different key name for a given situation.
For example, MongoDB uses `_id` while in Better-Auth we use `id`.
Each key in the returned object represents the old key to replace.
The value represents the new key.
This can be a partial object that only transforms some keys.
```ts title="Example"
mapKeysTransformOutput: () => {
return {
_id: "id", // We want to replace `_id` (from MongoDB) to `id` (for Better-Auth)
};
},
```
### `customTransformInput`
If you need to transform the input data before it is saved to the database, you can use this option to transform the data.
If you're using `supportsJSON`, `supportsDates`, or `supportsBooleans`, then
the transformations will be applied before your `customTransformInput`
function is called.
The `customTransformInput` function receives the following arguments:
* `data`: The data to transform.
* `field`: The field that is being transformed.
* `fieldAttributes`: The field attributes of the field that is being transformed.
* `select`: The `select` values which the query expects to return.
* `model`: The model that is being transformed.
* `schema`: The schema that is being transformed.
* `options`: Better Auth options.
The `customTransformInput` function runs at every key in the data object of a given action.
```ts title="Example"
customTransformInput: ({ field, data }) => {
if (field === "id") {
return "123"; // Force the ID to be "123"
}
return data;
};
```
### `customTransformOutput`
If you need to transform the output data before it is returned to the user, you can use this option to transform the data. The `customTransformOutput` function is used to transform the output data.
Similar to the `customTransformInput` function, it runs at every key in the data object of a given action, but it runs after the data is retrieved from the database.
```ts title="Example"
customTransformOutput: ({ field, data }) => {
if (field === "name") {
return "Bob"; // Force the name to be "Bob"
}
return data;
};
```
```ts
const some_data = await adapter.create({
model: "user",
data: {
name: "John",
},
});
// The name will be "Bob"
console.log(some_data.name);
```
# guides: Migrating from NextAuth.js to Better Auth
URL: /docs/guides/next-auth-migration-guide
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/next-auth-migration-guide.mdx
A step-by-step guide to transitioning from NextAuth.js to Better Auth.
***
title: Migrating from NextAuth.js to Better Auth
description: A step-by-step guide to transitioning from NextAuth.js to Better Auth.
-----------------------------------------------------------------------------------
In this guide, we’ll walk through the steps to migrate a project from [NextAuth.js](https://authjs.dev/) to Better Auth, ensuring no loss of data or functionality. While this guide focuses on Next.js, it can be adapted for other frameworks as well.
***
## Before You Begin
Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started.
***
### Mapping Existing Columns
Instead of altering your existing database column names, you can map them to match Better Auth's expected structure. This allows you to retain your current database schema.
#### User Schema
Map the following fields in the user schema:
* (next-auth v4) `emailVerified`: datetime → boolean
#### Session Schema
Map the following fields in the session schema:
* `expires` → `expiresAt`
* `sessionToken` → `token`
* (next-auth v4) add `createdAt` with datetime type
* (next-auth v4) add `updatedAt` with datetime type
```typescript title="auth.ts"
export const auth = betterAuth({
// Other configs
session: {
fields: {
expiresAt: "expires", // Map your existing `expires` field to Better Auth's `expiresAt`
token: "sessionToken" // Map your existing `sessionToken` field to Better Auth's `token`
}
},
});
```
Make sure to have `createdAt` and `updatedAt` fields on your session schema.
#### Account Schema
Map these fields in the account schema:
* (next-auth v4) `provider` → `providerId`
* `providerAccountId` → `accountId`
* `refresh_token` → `refreshToken`
* `access_token` → `accessToken`
* (next-auth v3) `access_token_expires` → `accessTokenExpiresAt` and int → datetime
* (next-auth v4) `expires_at` → `accessTokenExpiresAt` and int → datetime
* `id_token` → `idToken`
* (next-auth v4) add `createdAt` with datetime type
* (next-auth v4) add `updatedAt` with datetime type
Remove the `session_state`, `type`, and `token_type` fields, as they are not required by Better Auth.
```typescript title="auth.ts"
export const auth = betterAuth({
// Other configs
account: {
fields: {
accountId: "providerAccountId",
refreshToken: "refresh_token",
accessToken: "access_token",
accessTokenExpiresAt: "access_token_expires",
idToken: "id_token",
}
},
});
```
**Note:** If you use ORM adapters, you can map these fields in your schema file.
**Example with Prisma:**
```prisma title="schema.prisma"
model Session {
id String @id @default(cuid())
expiresAt DateTime @map("expires") // Map your existing `expires` field to Better Auth's `expiresAt`
token String @map("sessionToken") // Map your existing `sessionToken` field to Better Auth's `token`
userId String
user User @relation(fields: [userId], references: [id])
}
```
Make sure to have `createdAt` and `updatedAt` fields on your account schema.
### Update the Route Handler
In the `app/api/auth` folder, rename the `[...nextauth]` file to `[...all]` to avoid confusion. Then, update the `route.ts` file as follows:
```typescript title="app/api/auth/[...all]/route.ts"
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "~/server/auth";
export const { POST, GET } = toNextJsHandler(auth);
```
### Update the Client
Create a file named `auth-client.ts` in the `lib` folder. Add the following code:
```typescript title="auth-client.ts"
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.BASE_URL! // Optional if the API base URL matches the frontend
});
export const { signIn, signOut, useSession } = authClient;
```
#### Social Login Functions
Update your social login functions to use Better Auth. For example, for Discord:
```typescript
import { signIn } from "~/lib/auth-client";
export const signInDiscord = async () => {
const data = await signIn.social({
provider: "discord"
});
return data;
};
```
#### Update `useSession` Calls
Replace `useSession` calls with Better Auth’s version. Example:
```typescript title="Profile.tsx"
import { useSession } from "~/lib/auth-client";
export const Profile = () => {
const { data } = useSession();
return (
{JSON.stringify(data, null, 2)}
);
};
```
### Server-Side Session Handling
Use the `auth` instance to get session data on the server:
```typescript title="actions.ts"
"use server";
import { auth } from "~/server/auth";
import { headers } from "next/headers";
export const protectedAction = async () => {
const session = await auth.api.getSession({
headers: await headers(),
});
};
```
### Middleware
To protect routes with middleware, refer to the [Next.js middleware guide](/docs/integrations/next#middleware).
## Wrapping Up
Congratulations! You’ve successfully migrated from NextAuth.js to Better Auth. For a complete implementation with multiple authentication methods, check out the [demo repository](https://github.com/Bekacru/t3-app-better-auth).
Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential.
# guides: Optimizing for Performance
URL: /docs/guides/optimizing-for-performance
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/optimizing-for-performance.mdx
A guide to optimizing your Better Auth application for performance.
***
title: Optimizing for Performance
description: A guide to optimizing your Better Auth application for performance.
--------------------------------------------------------------------------------
In this guide, we’ll go over some of the ways you can optimize your application for a more performant Better Auth app.
## Caching
Caching is a powerful technique that can significantly improve the performance of your Better Auth application by reducing the number of database queries and speeding up response times.
### Cookie Cache
Calling your database every time `useSession` or `getSession` is invoked isn’t ideal, especially if sessions don’t change frequently. Cookie caching handles this by storing session data in a short-lived, signed cookie similar to how JWT access tokens are used with refresh tokens.
To turn on cookie caching, just set `session.cookieCache` in your auth config:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache duration in seconds
},
},
});
```
Read more about [cookie caching](/docs/concepts/session-management#cookie-cache).
### Framework Caching
Here are examples of how you can do caching in different frameworks and environments:
Since Next v15, we can use the `"use cache"` directive to cache the response of a server function.
```ts
export async function getUsers() {
'use cache' // [!code highlight]
const { users } = await auth.api.listUsers();
return users
}
```
Learn more about NextJS use cache directive here.
In Remix, you can use the `cache` option in the `loader` function to cache responses on the server. Here’s an example:
```ts
import { json } from '@remix-run/node';
export const loader = async () => {
const { users } = await auth.api.listUsers();
return json(users, {
headers: {
'Cache-Control': 'max-age=3600', // Cache for 1 hour
},
});
};
```
You can read a nice guide on Loader vs Route Cache Headers in Remix here.
In SolidStart, you can use the `query` function to cache data. Here’s an example:
```tsx
const getUsers = query(
async () => (await auth.api.listUsers()).users,
"getUsers"
);
```
Learn more about SolidStart `query` function here.
With React Query you can use the `useQuery` hook to cache data. Here’s an example:
```ts
import { useQuery } from '@tanstack/react-query';
const fetchUsers = async () => {
const { users } = await auth.api.listUsers();
return users;
};
export default function Users() {
const { data: users, isLoading } = useQuery('users', fetchUsers, {
staleTime: 1000 * 60 * 15, // Cache for 15 minutes
});
if (isLoading) return
Loading...
;
return (
{users.map(user => (
{user.name}
))}
);
}
```
Learn more about React Query use cache directive here.
## SSR Optimizations
If you're using a framework that supports server-side rendering, it's usually best to pre-fetch the user session on the server and use it as a fallback on the client.
```ts
const session = await auth.api.getSession({
headers: await headers(),
});
//then pass the session to the client
```
## Database optimizations
Optimizing database performance is essential to get the best out of Better Auth.
#### Recommended fields to index
| Table | Fields | Plugin |
| ------------- | -------------------------- | ------------ |
| users | `email` | |
| accounts | `userId` | |
| sessions | `userId`, `token` | |
| verifications | `identifier` | |
| invitations | `email`, `organizationId` | organization |
| members | `userId`, `organizationId` | organization |
| organizations | `slug` | organization |
| passkey | `userId` | passkey |
| twoFactor | `secret` | twoFactor |
We intend to add indexing support in our schema generation tool in the future.
# guides: Migrating from Supabase Auth to Better Auth
URL: /docs/guides/supabase-migration-guide
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/supabase-migration-guide.mdx
A step-by-step guide to transitioning from Supabase Auth to Better Auth.
***
title: Migrating from Supabase Auth to Better Auth
description: A step-by-step guide to transitioning from Supabase Auth to Better Auth.
-------------------------------------------------------------------------------------
In this guide, we'll walk through the steps to migrate a project from Supabase Auth to Better Auth.
This migration will invalidate all active sessions. While this guide doesn't currently cover migrating two-factor (2FA) or Row Level Security (RLS) configurations, both should be possible with additional steps.
## Before You Begin
Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started.
### Connect to your database
You'll need to connect to your database to migrate the users and accounts. Copy your `DATABASE_URL` from your Supabase project and use it to connect to your database. And for this example, we'll need to install `pg` to connect to the database.
npm
pnpm
yarn
bun
```bash
npm install pg
```
```bash
pnpm add pg
```
```bash
yarn add pg
```
```bash
bun add pg
```
And then you can use the following code to connect to your database.
```ts title="auth.ts"
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
})
```
### Enable Email and Password (Optional)
Enable the email and password in your auth config.
```ts title="auth.ts"
import { admin, anonymous } from "better-auth/plugins";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailVerification: {
sendEmailVerification: async(user)=>{
// send email verification email
// implement your own logic here
}
},
emailAndPassword: { // [!code highlight]
enabled: true, // [!code highlight]
} // [!code highlight]
})
```
### Setup Social Providers (Optional)
Add social providers you have enabled in your Supabase project in your auth config.
```ts title="auth.ts"
import { admin, anonymous } from "better-auth/plugins";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: { // [!code highlight]
github: { // [!code highlight]
clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight]
clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight]
} // [!code highlight]
} // [!code highlight]
})
```
### Add admin and anonymous plugins (Optional)
Add the [admin](/docs/plugins/admin) and [anonymous](/docs/plugins/anonymous) plugins to your auth config.
```ts title="auth.ts"
import { admin, anonymous } from "better-auth/plugins";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
},
plugins: [admin(), anonymous()], // [!code highlight]
})
```
### Run the migration
Run the migration to create the necessary tables in your database.
```bash title="Terminal"
npx @better-auth/cli migrate
```
This will create the following tables in your database:
* [`user`](/docs/concepts/database#user)
* [`account`](/docs/concepts/database#account)
* [`session`](/docs/concepts/database#session)
* [`verification`](/docs/concepts/database#verification)
This tables will be created on the `public` schema.
### Copy the migration script
Now that we have the necessary tables in our database, we can run the migration script to migrate the users and accounts from Supabase to Better Auth.
Start by creating a `.ts` file in your project.
```bash title="Terminal"
touch migration.ts
```
And then copy and paste the following code into the file.
```ts title="migration.ts"
import { Pool } from "pg";
import { auth } from "./auth";
import { User as SupabaseUser } from "@supabase/supabase-js";
type User = SupabaseUser & {
is_super_admin: boolean;
raw_user_meta_data: {
avatar_url: string;
};
encrypted_password: string;
email_confirmed_at: string;
created_at: string;
updated_at: string;
is_anonymous: boolean;
identities: {
provider: string;
identity_data: {
sub: string;
email: string;
};
created_at: string;
updated_at: string;
};
};
const migrateFromSupabase = async () => {
const ctx = await auth.$context;
const db = ctx.options.database as Pool;
const users = await db
.query(`
SELECT
u.*,
COALESCE(
json_agg(
i.* ORDER BY i.id
) FILTER (WHERE i.id IS NOT NULL),
'[]'::json
) as identities
FROM auth.users u
LEFT JOIN auth.identities i ON u.id = i.user_id
GROUP BY u.id
`)
.then((res) => res.rows as User[]);
for (const user of users) {
if (!user.email) {
continue;
}
await ctx.adapter
.create({
model: "user",
data: {
id: user.id,
email: user.email,
name: user.email,
role: user.is_super_admin ? "admin" : user.role,
emailVerified: !!user.email_confirmed_at,
image: user.raw_user_meta_data.avatar_url,
createdAt: new Date(user.created_at),
updatedAt: new Date(user.updated_at),
isAnonymous: user.is_anonymous,
},
})
.catch(() => {});
for (const identity of user.identities) {
const existingAccounts = await ctx.internalAdapter.findAccounts(user.id);
if (identity.provider === "email") {
const hasCredential = existingAccounts.find(
(account) => account.providerId === "credential",
);
if (!hasCredential) {
await ctx.adapter
.create({
model: "account",
data: {
userId: user.id,
providerId: "credential",
accountId: user.id,
password: user.encrypted_password,
createdAt: new Date(user.created_at),
updatedAt: new Date(user.updated_at),
},
})
.catch(() => {});
}
}
const supportedProviders = Object.keys(ctx.options.socialProviders || {})
if (supportedProviders.includes(identity.provider)) {
const hasAccount = existingAccounts.find(
(account) => account.providerId === identity.provider,
);
if (!hasAccount) {
await ctx.adapter.create({
model: "account",
data: {
userId: user.id,
providerId: identity.provider,
accountId: identity.identity_data?.sub,
createdAt: new Date(identity.created_at ?? user.created_at),
updatedAt: new Date(identity.updated_at ?? user.updated_at),
},
});
}
}
}
}
};
migrateFromSupabase();
```
### Customize the migration script (Optional)
* `name`: the migration script will use the user's email as the name. You might want to customize it if you have the user display name in your database.
* `socialProviderList`: the migration script will use the social providers you have enabled in your auth config. You might want to customize it if you have additional social providers that you haven't enabled in your auth config.
* `role`: remove `role` if you're not using the `admin` plugin
* `isAnonymous`: remove `isAnonymous` if you're not using the `anonymous` plugin.
* update other tables that reference the `users` table to use the `id` field.
### Run the migration script
Run the migration script to migrate the users and accounts from Supabase to Better Auth.
```bash title="Terminal"
bun migration.ts # or use node, ts-node, etc.
```
### Update your code
Update your codebase from Supabase auth calls to Better Auth API.
Here's a list of the Supabase auth API calls and their Better Auth counterparts.
* `supabase.auth.signUp` -> `authClient.signUp.email`
* `supabase.auth.signInWithPassword` -> `authClient.signIn.email`
* `supabase.auth.signInWithOAuth` -> `authClient.signIn.social`
* `supabase.auth.signInAnonymously` -> `authClient.signIn.anonymous`
* `supabase.auth.signOut` -> `authClient.signOut`
* `supabase.auth.getSession` -> `authClient.getSession` - you can also use `authClient.useSession` for reactive state
Learn more:
* [Basic Usage](/docs/basic-usage): Learn how to use the auth client to sign up, sign in, and sign out.
* [Email and Password](/docs/authentication/email-and-password): Learn how to add email and password authentication to your project.
* [Anonymous](/docs/plugins/anonymous): Learn how to add anonymous authentication to your project.
* [Admin](/docs/plugins/admin): Learn how to add admin authentication to your project.
* [Email OTP](/docs/authentication/email-otp): Learn how to add email OTP authentication to your project.
* [Hooks](/docs/concepts/hooks): Learn how to use the hooks to listen for events.
* [Next.js](/docs/integrations/next): Learn how to use the auth client in a Next.js project.
### Middleware
To protect routes with middleware, refer to the [Next.js middleware guide](/docs/integrations/next#middleware) or your framework's documentation.
## Wrapping Up
Congratulations! You've successfully migrated from Supabase Auth to Better Auth.
Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential.
# guides: Create your first plugin
URL: /docs/guides/your-first-plugin
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/guides/your-first-plugin.mdx
A step-by-step guide to creating your first Better Auth plugin.
***
title: Create your first plugin
description: A step-by-step guide to creating your first Better Auth plugin.
----------------------------------------------------------------------------
In this guide, we’ll walk you through the steps of creating your first Better Auth plugin.
This guide assumes you have setup the basics of Better Auth and are ready to create your first plugin.
## Plan your idea
Before beginning, you must know what plugin you intend to create.
In this guide, we’ll create a **birthday plugin** to keep track of user birth dates.
## Server plugin first
Better Auth plugins operate as a pair: a server plugin and a client plugin.
The server plugin forms the foundation of your authentication system, while the client plugin provides convenient frontend APIs to interact with your server implementation.
You can read more about server/client plugins in our documentation.
### Creating the server plugin
Go ahead and find a suitable location to create your birthday plugin folder, with an `index.ts` file within.
In the `index.ts` file, we’ll export a function that represents our server plugin.
This will be what we will later add to our plugin list in the `auth.ts` file.
```ts title="index.ts"
import { createAuthClient } from "better-auth/client";
import type { BetterAuthPlugin } from "better-auth";
export const birthdayPlugin = () =>
({
id: "birthdayPlugin",
} satisfies BetterAuthPlugin);
```
Although this does nothing, you have technically just made yourself your first plugin, congratulations! 🎉
### Defining a schema
In order to save each user’s birthday data, we must create a schema on top of the `user` model.
By creating a schema here, this also allows Better Auth’s CLI to generate the schemas required to update your database.
You can learn more about plugin schemas here.
```ts title="index.ts"
//...
export const birthdayPlugin = () =>
({
id: "birthdayPlugin",
schema: {// [!code highlight]
user: {// [!code highlight]
fields: {// [!code highlight]
birthday: {// [!code highlight]
type: "date", // string, number, boolean, date // [!code highlight]
required: true, // if the field should be required on a new record. (default: false) // [!code highlight]
unique: false, // if the field should be unique. (default: false) // [!code highlight]
references: null // if the field is a reference to another table. (default: null) // [!code highlight]
},// [!code highlight]
},// [!code highlight]
},// [!code highlight]
},
} satisfies BetterAuthPlugin);
```
### Authorization logic
For this example guide, we’ll set up authentication logic to check and ensure that the user who signs-up is older than 5.
But the same concept could be applied for something like verifying users agreeing to the TOS or anything alike.
To do this, we’ll utilize Hooks, which allows us to run code `before` or `after` an action is performed.
```ts title="index.ts"
export const birthdayPlugin = () => ({
//...
// In our case, we want to write authorization logic,
// meaning we want to intercept it `before` hand.
hooks: {
before: [
{
matcher: (context) => /* ... */,
handler: createAuthMiddleware(async (ctx) => {
//...
}),
},
],
},
} satisfies BetterAuthPlugin)
```
In our case we want to match any requests going to the signup path:
```ts title="Before hook"
{
matcher: (context) => context.path.startsWith("/sign-up/email"),
//...
}
```
And for our logic, we’ll write the following code to check the if user’s birthday makes them above 5 years old.
```ts title="Imports"
import { APIError } from "better-auth/api";
import { createAuthMiddleware } from "better-auth/plugins";
```
```ts title="Before hook"
{
//...
handler: createAuthMiddleware(async (ctx) => {
const { birthday } = ctx.body;
if(!(birthday instanceof Date)) {
throw new APIError("BAD_REQUEST", { message: "Birthday must be of type Date." });
}
const today = new Date();
const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5));
if(birthday >= fiveYearsAgo) {
throw new APIError("BAD_REQUEST", { message: "User must be above 5 years old." });
}
return { context: ctx };
}),
}
```
**Authorized!** 🔒
We’ve now successfully written code to ensure authorization for users above 5!
## Client Plugin
We’re close to the finish line! 🏁
Now that we have created our server plugin, the next step is to develop our client plugin.
Since there isn’t much frontend APIs going on for this plugin, there isn’t much to do!
First, let’s create our `client.ts` file first:
Then, add the following code:
```ts title="client.ts"
import { BetterAuthClientPlugin } from "better-auth";
import type { birthdayPlugin } from "./index"; // make sure to import the server plugin as a type // [!code highlight]
type BirthdayPlugin = typeof birthdayPlugin;
export const birthdayClientPlugin = () => {
return {
id: "birthdayPlugin",
$InferServerPlugin: {} as ReturnType,
} satisfies BetterAuthClientPlugin;
};
```
What we’ve done is allow the client plugin to infer the types defined by our schema from the server plugin.
And that’s it! This is all it takes for the birthday client plugin. 🎂
## Initiate your plugin!
Both the `client` and `server` plugins are now ready, the last step is to import them to both your `auth-client.ts` and your `server.ts` files respectively to initiate the plugin.
### Server initiation
```ts title="server.ts"
import { betterAuth } from "better-auth";
import { birthdayPlugin } from "./birthday-plugin";// [!code highlight]
export const auth = betterAuth({
plugins: [
birthdayPlugin(),// [!code highlight]
]
});
```
### Client initiation
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { birthdayClientPlugin } from "./birthday-plugin/client";// [!code highlight]
const authClient = createAuthClient({
plugins: [
birthdayClientPlugin()// [!code highlight]
]
});
```
### Oh yeah, the schemas!
Don’t forget to add your `birthday` field to your `user` table model!
Or, use the `generate` CLI command:
```bash
npx @better-auth/cli@latest generate
```
## Wrapping Up
Congratulations! You’ve successfully created your first ever Better Auth plugin.
We highly recommend you visit our plugins documentation to learn more information.
If you have a plugin you’d like to share with the community, feel free to let us know through
our Discord server,
or through a pull-request
and we may add it to the community-plugins list!
# examples: Astro Example
URL: /docs/examples/astro
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/examples/astro.mdx
Better Auth Astro example.
***
title: Astro Example
description: Better Auth Astro example.
---------------------------------------
This is an example of how to use Better Auth with Astro. It uses Solid for building the components.
**Implements the following features:**
Email & Password . Social Sign-in with Google . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management
## How to run
1. Clone the code sandbox (or the repo) and open it in your code editor
2. Provide .env file with the following variables
```txt
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
BETTER_AUTH_SECRET=
```
//if you don't have these, you can get them from the google developer console. If you don't want to use google sign-in, you can remove the google config from the `auth.ts` file.
3. Run the following commands
```bash
pnpm install
pnpm run dev
```
4. Open the browser and navigate to `http://localhost:3000`
# examples: Next.js Example
URL: /docs/examples/next-js
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/examples/next-js.mdx
Better Auth Next.js example.
***
title: Next.js Example
description: Better Auth Next.js example.
-----------------------------------------
This is an example of how to use Better Auth with Next.
**Implements the following features:**
Email & Password . Social Sign-in . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management . Organization, Members and Roles
See [Demo](https://demo.better-auth.com)
## How to run
1. Clone the code sandbox (or the repo) and open it in your code editor
2. Move .env.example to .env and provide necessary variables
3. Run the following commands
```bash
pnpm install
pnpm dev
```
4. Open the browser and navigate to `http://localhost:3000`
# examples: Nuxt Example
URL: /docs/examples/nuxt
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/examples/nuxt.mdx
Better Auth Nuxt example.
***
title: Nuxt Example
description: Better Auth Nuxt example.
--------------------------------------
This is an example of how to use Better Auth with Nuxt.
**Implements the following features:**
Email & Password . Social Sign-in with Google
## How to run
1. Clone the code sandbox (or the repo) and open it in your code editor
2. Move .env.example to .env and provide necessary variables
3. Run the following commands
```bash
pnpm install
pnpm dev
```
4. Open the browser and navigate to `http://localhost:3000`
# examples: Remix Example
URL: /docs/examples/remix
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/examples/remix.mdx
Better Auth Remix example.
***
title: Remix Example
description: Better Auth Remix example.
---------------------------------------
This is an example of how to use Better Auth with Remix.
**Implements the following features:**
Email & Password . Social Sign-in with Google . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management
## How to run
1. Clone the code sandbox (or the repo) and open it in your code editor
2. Provide .env file with by copying the `.env.example` file and adding the variables
3. Run the following commands
```bash
pnpm install
pnpm run dev
```
4. Open the browser and navigate to `http://localhost:3000`
# examples: SvelteKit Example
URL: /docs/examples/svelte-kit
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/examples/svelte-kit.mdx
Better Auth SvelteKit example.
***
title: SvelteKit Example
description: Better Auth SvelteKit example.
-------------------------------------------
This is an example of how to use Better Auth with SvelteKit.
**Implements the following features:**
Email & Password . Social Sign-in with Google . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management
## How to run
1. Clone the code sandbox (or the repo) and open it in your code editor
2. Move .env.example to .env and provide necessary variables
3. Run the following commands
```bash
pnpm install
pnpm dev
```
4. Open the browser and navigate to `http://localhost:3000`
# integrations: Astro Integration
URL: /docs/integrations/astro
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/astro.mdx
Integrate Better Auth with Astro.
***
title: Astro Integration
description: Integrate Better Auth with Astro.
----------------------------------------------
Better Auth comes with first class support for Astro. This guide will show you how to integrate Better Auth with Astro.
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
To enable Better Auth to handle requests, we need to mount the handler to a catch all API route. Create a file inside `/pages/api/auth` called `[...all].ts` and add the following code:
```ts title="pages/api/auth/[...all].ts"
import { auth } from "~/auth";
import type { APIRoute } from "astro";
export const ALL: APIRoute = async (ctx) => {
// If you want to use rate limiting, make sure to set the 'x-forwarded-for' header to the request headers from the context
// ctx.request.headers.set("x-forwarded-for", ctx.clientAddress);
return auth.handler(ctx.request);
};
```
You can change the path on your better-auth configuration but it's recommended to keep it as `/api/auth/[...all]`
## Create a client
Astro supports multiple frontend frameworks, so you can easily import your client based on the framework you're using.
If you're not using a frontend framework, you can still import the vanilla client.
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/client"
export const authClient = createAuthClient()
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient()
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/vue"
export const authClient = createAuthClient()
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/svelte"
export const authClient = createAuthClient()
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/solid"
export const authClient = createAuthClient()
```
## Auth Middleware
### Astro Locals types
To have types for your Astro locals, you need to set it inside the `env.d.ts` file.
```ts title="env.d.ts"
///
declare namespace App {
// Note: 'import {} from ""' syntax does not work in .d.ts files.
interface Locals {
user: import("better-auth").User | null;
session: import("better-auth").Session | null;
}
}
```
### Middleware
To protect your routes, you can check if the user is authenticated using the `getSession` method in middleware and set the user and session data using the Astro locals with the types we set before. Start by creating a `middleware.ts` file in the root of your project and follow the example below:
```ts title="middleware.ts"
import { auth } from "@/auth";
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => {
const isAuthed = await auth.api
.getSession({
headers: context.request.headers,
})
if (isAuthed) {
context.locals.user = isAuthed.user;
context.locals.session = isAuthed.session;
} else {
context.locals.user = null;
context.locals.session = null;
}
return next();
});
```
### Getting session on the server inside `.astro` file
You can use `Astro.locals` to check if the user has session and get the user data from the server side. Here is an example of how you can get the session inside an `.astro` file:
```astro
---
import { UserCard } from "@/components/user-card";
const session = () => {
if (Astro.locals.session) {
return Astro.locals.session;
} else {
// Redirect to login page if the user is not authenticated
return Astro.redirect("/login");
}
}
---
```
# integrations: Elysia Integration
URL: /docs/integrations/elysia
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/elysia.mdx
Integrate Better Auth with Elysia.
***
title: Elysia Integration
description: Integrate Better Auth with Elysia.
-----------------------------------------------
This integration guide is assuming you are using Elysia with bun server.
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
We need to mount the handler to Elysia endpoint.
```ts
import { Elysia } from "elysia";
import { auth } from "./auth";
const app = new Elysia().mount(auth.handler).listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);
```
### CORS
To configure cors, you can use the `cors` plugin from `@elysiajs/cors`.
```ts
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { auth } from "./auth";
const app = new Elysia()
.use(
cors({
origin: "http://localhost:3001",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true,
allowedHeaders: ["Content-Type", "Authorization"],
}),
)
.mount(auth.handler)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);
```
### Macro
You can use [macro](https://elysiajs.com/patterns/macro.html#macro) with [resolve](https://elysiajs.com/essential/handler.html#resolve) to provide session and user information before pass to view.
```ts
import { Elysia } from "elysia";
import { auth } from "./auth";
// user middleware (compute user and session and pass to routes)
const betterAuth = new Elysia({ name: "better-auth" })
.mount(auth.handler)
.macro({
auth: {
async resolve({ status, request: { headers } }) {
const session = await auth.api.getSession({
headers,
});
if (!session) return status(401);
return {
user: session.user,
session: session.session,
};
},
},
});
const app = new Elysia()
.use(betterAuth)
.get("/user", ({ user }) => user, {
auth: true,
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);
```
This will allow you to access the `user` and `session` object in all of your routes.
# integrations: Expo Integration
URL: /docs/integrations/expo
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/expo.mdx
Integrate Better Auth with Expo.
***
title: Expo Integration
description: Integrate Better Auth with Expo.
---------------------------------------------
Expo is a popular framework for building cross-platform apps with React Native. Better Auth supports both Expo native and web apps.
## Installation
## Configure A Better Auth Backend
Before using Better Auth with Expo, make sure you have a Better Auth backend set up. You can either use a separate server or leverage Expo's new [API Routes](https://docs.expo.dev/router/reference/api-routes) feature to host your Better Auth instance.
To get started, check out our [installation](/docs/installation) guide for setting up Better Auth on your server. If you prefer to check out the full example, you can find it [here](https://github.com/better-auth/examples/tree/main/expo-example).
To use the new API routes feature in Expo to host your Better Auth instance you can create a new API route in your Expo app and mount the Better Auth handler.
```ts title="app/api/auth/[...auth]+api.ts"
import { auth } from "@/lib/auth"; // import Better Auth handler
const handler = auth.handler;
export { handler as GET, handler as POST }; // export handler for both GET and POST requests
```
## Install Server Dependencies
Install both the Better Auth package and Expo plugin into your server application.
npm
pnpm
yarn
bun
```bash
npm install better-auth @better-auth/expo
```
```bash
pnpm add better-auth @better-auth/expo
```
```bash
yarn add better-auth @better-auth/expo
```
```bash
bun add better-auth @better-auth/expo
```
## Install Client Dependencies
You also need to install both the Better Auth package and Expo plugin into your Expo application.
npm
pnpm
yarn
bun
```bash
npm install better-auth @better-auth/expo
```
```bash
pnpm add better-auth @better-auth/expo
```
```bash
yarn add better-auth @better-auth/expo
```
```bash
bun add better-auth @better-auth/expo
```
If you plan on using our social integrations (Google, Apple etc.) then there are a few more dependencies that are required in your Expo app. In the default Expo template these are already installed so you may be able to skip this step if you have these dependencies already.
npm
pnpm
yarn
bun
```bash
npm install expo-linking expo-web-browser expo-constants
```
```bash
pnpm add expo-linking expo-web-browser expo-constants
```
```bash
yarn add expo-linking expo-web-browser expo-constants
```
```bash
bun add expo-linking expo-web-browser expo-constants
```
## Add the Expo Plugin on Your Server
Add the Expo plugin to your Better Auth server.
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth";
import { expo } from "@better-auth/expo";
export const auth = betterAuth({
plugins: [expo()],
emailAndPassword: {
enabled: true, // Enable authentication using email and password.
},
});
```
## Initialize Better Auth Client
To initialize Better Auth in your Expo app, you need to call `createAuthClient` with the base URL of your Better Auth backend. Make sure to import the client from `/react`.
Make sure you install the `expo-secure-store` package into your Expo app. This is used to store the session data and cookies securely.
npm
pnpm
yarn
bun
```bash
npm install expo-secure-store
```
```bash
pnpm add expo-secure-store
```
```bash
yarn add expo-secure-store
```
```bash
bun add expo-secure-store
```
You need to also import client plugin from `@better-auth/expo/client` and pass it to the `plugins` array when initializing the auth client.
This is important because:
* **Social Authentication Support:** enables social auth flows by handling authorization URLs and callbacks within the Expo web browser.
* **Secure Cookie Management:** stores cookies securely and automatically adds them to the headers of your auth requests.
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081", // Base URL of your Better Auth backend.
plugins: [
expoClient({
scheme: "myapp",
storagePrefix: "myapp",
storage: SecureStore,
})
]
});
```
Be sure to include the full URL, including the path, if you've changed the default path from `/api/auth`.
## Scheme and Trusted Origins
Better Auth uses deep links to redirect users back to your app after authentication. To enable this, you need to add your app's scheme to the `trustedOrigins` list in your Better Auth config.
First, make sure you have a scheme defined in your `app.json` file.
```json title="app.json"
{
"expo": {
"scheme": "myapp"
}
}
```
Then, update your Better Auth config to include the scheme in the `trustedOrigins` list.
```ts title="auth.ts"
export const auth = betterAuth({
trustedOrigins: ["myapp://"]
})
```
If you have multiple schemes or need to support deep linking with various paths, you can use specific patterns or wildcards:
```ts title="auth.ts"
export const auth = betterAuth({
trustedOrigins: [
// Basic scheme
"myapp://",
// Production & staging schemes
"myapp-prod://",
"myapp-staging://",
// Wildcard support for all paths following the scheme
"myapp://*"
]
})
```
The wildcard pattern can be particularly useful if your app uses different URL formats for deep linking based on features or screens.
## Configure Metro Bundler
To resolve Better Auth exports you'll need to enable `unstable_enablePackageExports` in your metro config.
```js title="metro.config.js"
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname)
config.resolver.unstable_enablePackageExports = true; // [!code highlight]
module.exports = config;
```
In case you don't have a `metro.config.js` file in your project run `npx expo customize metro.config.js`.
If you can't enable `unstable_enablePackageExports` option, you can use [babel-plugin-module-resolver](https://github.com/tleunen/babel-plugin-module-resolver) to manually resolve the paths.
```ts title="babel.config.js"
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [
[
"module-resolver",
{
alias: {
"better-auth/react": "./node_modules/better-auth/dist/client/react/index.cjs",
"better-auth/client/plugins": "./node_modules/better-auth/dist/client/plugins/index.cjs",
"@better-auth/expo/client": "./node_modules/@better-auth/expo/dist/client.cjs",
},
},
],
],
}
}
```
In case you don't have a `babel.config.js` file in your project run `npx expo customize babel.config.js`.
Don't forget to clear the cache after making changes.
```bash
npx expo start --clear
```
## Usage
### Authenticating Users
With Better Auth initialized, you can now use the `authClient` to authenticate users in your Expo app.
```tsx title="app/sign-in.tsx"
import { useState } from "react";
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";
export default function SignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
await authClient.signIn.email({
email,
password,
})
};
return (
);
}
```
```tsx title="app/sign-up.tsx"
import { useState } from "react";
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";
export default function SignUp() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
await authClient.signUp.email({
email,
password,
name
})
};
return (
);
}
```
#### Social Sign-In
For social sign-in, you can use the `authClient.signIn.social` method with the provider name and a callback URL.
```tsx title="app/social-sign-in.tsx"
import { Button } from "react-native";
export default function SocialSignIn() {
const handleLogin = async () => {
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native
})
};
return ;
}
```
#### IdToken Sign-In
If you want to make provider request on the mobile device and then verify the ID token on the server, you can use the `authClient.signIn.social` method with the `idToken` option.
```tsx title="app/social-sign-in.tsx"
import { Button } from "react-native";
export default function SocialSignIn() {
const handleLogin = async () => {
await authClient.signIn.social({
provider: "google", // only google, apple and facebook are supported for idToken signIn
idToken: {
token: "...", // ID token from provider
nonce: "...", // nonce from provider (optional)
}
callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native
})
};
return ;
}
```
### Session
Better Auth provides a `useSession` hook to access the current user's session in your app.
```tsx title="app/index.tsx"
import { Text } from "react-native";
import { authClient } from "@/lib/auth-client";
export default function Index() {
const { data: session } = authClient.useSession();
return Welcome, {session?.user.name};
}
```
On native, the session data will be cached in SecureStore. This will allow you to remove the need for a loading spinner when the app is reloaded. You can disable this behavior by passing the `disableCache` option to the client.
### Making Authenticated Requests to Your Server
To make authenticated requests to your server that require the user's session, you have to retrieve the session cookie from `SecureStore` and manually add it to your request headers.
```tsx
import { authClient } from "@/lib/auth-client";
const makeAuthenticatedRequest = async () => {
const cookies = authClient.getCookie(); // [!code highlight]
const headers = {
"Cookie": cookies, // [!code highlight]
};
const response = await fetch("http://localhost:8081/api/secure-endpoint", {
headers,
// 'include' can interfere with the cookies we just set manually in the headers
credentials: "omit" // [!code highlight]
});
const data = await response.json();
return data;
};
```
**Example: Usage With TRPC**
```tsx title="lib/trpc-provider.tsx"
//...other imports
import { authClient } from "@/lib/auth-client"; // [!code highlight]
export const api = createTRPCReact();
export function TRPCProvider(props: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
api.createClient({
links: [
httpBatchLink({
//...your other options
headers() {
const headers = new Map(); // [!code highlight]
const cookies = authClient.getCookie(); // [!code highlight]
if (cookies) { // [!code highlight]
headers.set("Cookie", cookies); // [!code highlight]
} // [!code highlight]
return Object.fromEntries(headers); // [!code highlight]
},
}),
],
}),
);
return (
{props.children}
);
}
```
## Options
### Expo Client
**storage**: the storage mechanism used to cache the session data and cookies.
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react";
import SecureStorage from "expo-secure-store";
const authClient = createAuthClient({
baseURL: "http://localhost:8081",
storage: SecureStorage
});
```
**scheme**: scheme is used to deep link back to your app after a user has authenticated using oAuth providers. By default, Better Auth tries to read the scheme from the `app.json` file. If you need to override this, you can pass the scheme option to the client.
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react";
const authClient = createAuthClient({
baseURL: "http://localhost:8081",
scheme: "myapp"
});
```
**disableCache**: By default, the client will cache the session data in SecureStore. You can disable this behavior by passing the `disableCache` option to the client.
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react";
const authClient = createAuthClient({
baseURL: "http://localhost:8081",
disableCache: true
});
```
### Expo Servers
Server plugin options:
**overrideOrigin**: Override the origin for Expo API routes (default: false). Enable this if you're facing cors origin issues with Expo API routes.
# integrations: Express Integration
URL: /docs/integrations/express
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/express.mdx
Integrate Better Auth with Express.
***
title: Express Integration
description: Integrate Better Auth with Express.
------------------------------------------------
This guide will show you how to integrate Better Auth with [express.js](https://expressjs.com/).
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
Note that CommonJS (cjs) isn't supported. Use ECMAScript Modules (ESM) by setting `"type": "module"` in your `package.json` or configuring your `tsconfig.json` to use ES modules.
### Mount the handler
To enable Better Auth to handle requests, we need to mount the handler to an API route. Create a catch-all route to manage all requests to `/api/auth/*` in case of ExpressJS v4 or `/api/auth/*splat` in case of ExpressJS v5 (or any other path specified in your Better Auth options).
Don’t use `express.json()` before the Better Auth handler. Use it only for other routes, or the client API will get stuck on "pending".
```ts title="server.ts"
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";
const app = express();
const port = 3005;
app.all("/api/auth/*", toNodeHandler(auth)); // For ExpressJS v4
// app.all("/api/auth/*splat", toNodeHandler(auth)); For ExpressJS v5
// Mount express json middleware after Better Auth handler
// or only apply it to routes that don't interact with Better Auth
app.use(express.json());
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
```
After completing the setup, start your server. Better Auth will be ready to use. You can send a `GET` request to the `/ok` endpoint (`/api/auth/ok`) to verify that the server is running.
### Cors Configuration
To add CORS (Cross-Origin Resource Sharing) support to your Express server when integrating Better Auth, you can use the `cors` middleware. Below is an updated example showing how to configure CORS for your server:
```ts
import express from "express";
import cors from "cors"; // Import the CORS middleware
import { toNodeHandler, fromNodeHeaders } from "better-auth/node";
import { auth } from "./auth";
const app = express();
const port = 3005;
// Configure CORS middleware
app.use(
cors({
origin: "http://your-frontend-domain.com", // Replace with your frontend's origin
methods: ["GET", "POST", "PUT", "DELETE"], // Specify allowed HTTP methods
credentials: true, // Allow credentials (cookies, authorization headers, etc.)
})
);
```
### Getting the User Session
To retrieve the user's session, you can use the `getSession` method provided by the `auth` object. This method requires the request headers to be passed in a specific format. To simplify this process, Better Auth provides a `fromNodeHeaders` helper function that converts Node.js request headers to the format expected by Better Auth (a `Headers` object).
Here's an example of how to use `getSession` in an Express route:
```ts title="server.ts"
import { fromNodeHeaders } from "better-auth/node";
import { auth } from "./auth"; // Your Better Auth instance
app.get("/api/me", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
return res.json(session);
});
```
# integrations: Better Auth Fastify Integration Guide
URL: /docs/integrations/fastify
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/fastify.mdx
Learn how to seamlessly integrate Better Auth with your Fastify application.
***
title: Better Auth Fastify Integration Guide
description: Learn how to seamlessly integrate Better Auth with your Fastify application.
-----------------------------------------------------------------------------------------
This guide provides step-by-step instructions for configuring both essential handlers and CORS settings.
A configured Better Auth instance is required before proceeding. If you haven't set this up yet, please consult our [Installation Guide](/docs/installation).
### Prerequisites
Verify the following requirements before integration:
* **Node.js Environment**: v16 or later installed
* **ES Module Support**: Enable ES modules in either:
* `package.json`: `{ "type": "module" }`
* TypeScript `tsconfig.json`: `{ "module": "ESNext" }`
* **Fastify Dependencies**:
npm
pnpm
yarn
bun
```bash
npm install fastify @fastify/cors
```
```bash
pnpm add fastify @fastify/cors
```
```bash
yarn add fastify @fastify/cors
```
```bash
bun add fastify @fastify/cors
```
For TypeScript: Ensure your `tsconfig.json` includes `"esModuleInterop": true` for optimal compatibility.
### Authentication Handler Setup
Configure Better Auth to process authentication requests by creating a catch-all route:
```ts title="server.ts"
import Fastify from "fastify";
import { auth } from "./auth"; // Your configured Better Auth instance
const fastify = Fastify({ logger: true });
// Register authentication endpoint
fastify.route({
method: ["GET", "POST"],
url: "/api/auth/*",
async handler(request, reply) {
try {
// Construct request URL
const url = new URL(request.url, `http://${request.headers.host}`);
// Convert Fastify headers to standard Headers object
const headers = new Headers();
Object.entries(request.headers).forEach(([key, value]) => {
if (value) headers.append(key, value.toString());
});
// Create Fetch API-compatible request
const req = new Request(url.toString(), {
method: request.method,
headers,
body: request.body ? JSON.stringify(request.body) : undefined,
});
// Process authentication request
const response = await auth.handler(req);
// Forward response to client
reply.status(response.status);
response.headers.forEach((value, key) => reply.header(key, value));
reply.send(response.body ? await response.text() : null);
} catch (error) {
fastify.log.error("Authentication Error:", error);
reply.status(500).send({
error: "Internal authentication error",
code: "AUTH_FAILURE"
});
}
}
});
// Initialize server
fastify.listen({ port: 4000 }, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
console.log("Server running on port 4000");
});
```
### Trusted origins
When a request is made from a different origin, the request will be blocked by default. You can add trusted origins to the `auth` instance.
```ts
export const auth = betterAuth({
trustedOrigins: ["http://localhost:3000", "https://example.com"],
});
```
### Configuring CORS
Secure your API endpoints with proper CORS configuration:
```ts
import fastifyCors from "@fastify/cors";
// Configure CORS policies
fastify.register(fastifyCors, {
origin: process.env.CLIENT_ORIGIN || "http://localhost:3000",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"X-Requested-With"
],
credentials: true,
maxAge: 86400
});
// Mount authentication handler after CORS registration
// (Use previous handler configuration here)
```
Always restrict CORS origins in production environments. Use environment variables for dynamic configuration.
# integrations: Hono Integration
URL: /docs/integrations/hono
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/hono.mdx
Integrate Better Auth with Hono.
***
title: Hono Integration
description: Integrate Better Auth with Hono.
---------------------------------------------
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
We need to mount the handler to Hono endpoint.
```ts
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);
```
### Cors
To configure cors, you need to use the `cors` plugin from `hono/cors`.
```ts
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono();
app.use(
"/api/auth/*", // or replace with "*" to enable cors for all routes
cors({
origin: "http://localhost:3001", // replace with your origin
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);
```
> **Important:** CORS middleware must be registered before your routes. This ensures that cross-origin requests are properly handled before they reach your authentication endpoints.
### Middleware
You can add a middleware to save the `session` and `user` in a `context` and also add validations for every route.
```ts
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono<{
Variables: {
user: typeof auth.$Infer.Session.user | null;
session: typeof auth.$Infer.Session.session | null
}
}>();
app.use("*", async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
c.set("user", null);
c.set("session", null);
return next();
}
c.set("user", session.user);
c.set("session", session.session);
return next();
});
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);
```
This will allow you to access the `user` and `session` object in all of your routes.
```ts
app.get("/session", (c) => {
const session = c.get("session")
const user = c.get("user")
if(!user) return c.body(null, 401);
return c.json({
session,
user
});
});
```
### Cross-Domain Cookies
By default, all Better Auth cookies are set with `SameSite=Lax`. If you need to use cookies across different domains, you’ll need to set `SameSite=None` and `Secure=true`. However, we recommend using subdomains whenever possible, as this allows you to keep `SameSite=Lax`. To enable cross-subdomain cookies, simply turn on `crossSubDomainCookies` in your auth config.
```ts title="auth.ts"
export const auth = createAuth({
advanced: {
crossSubDomainCookies: {
enabled: true
}
}
})
```
If you still need to set `SameSite=None` and `Secure=true`, you can adjust these attributes globally through `cookieOptions` in the `createAuth` configuration.
```ts title="auth.ts"
export const auth = createAuth({
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
partitioned: true // New browser standards will mandate this for foreign cookies
}
}
})
```
You can also customize cookie attributes individually by setting them within `cookies` in your auth config.
```ts title="auth.ts"
export const auth = createAuth({
advanced: {
cookies: {
sessionToken: {
attributes: {
sameSite: "none",
secure: true,
partitioned: true // New browser standards will mandate this for foreign cookies
}
}
}
}
})
```
### Client-Side Configuration
When using the Hono client (`@hono/client`) to make requests to your Better Auth-protected endpoints, you need to configure it to send credentials (cookies) with cross-origin requests.
```ts title="api.ts"
import { hc } from "hono/client";
import type { AppType } from "./server"; // Your Hono app type
const client = hc("http://localhost:8787/", {
init: {
credentials: "include", // Required for sending cookies cross-origin
},
});
// Now your client requests will include credentials
const response = await client.someProtectedEndpoint.$get();
```
This configuration is necessary when:
* Your client and server are on different domains/ports during development
* You're making cross-origin requests in production
* You need to send authentication cookies with your requests
The `credentials: "include"` option tells the fetch client to send cookies even for cross-origin requests. This works in conjunction with the CORS configuration on your server that has `credentials: true`.
> **Note:** Make sure your CORS configuration on the server matches your client's domain, and that `credentials: true` is set in both the server's CORS config and the client's fetch config.
# integrations: Lynx Integration
URL: /docs/integrations/lynx
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/lynx.mdx
Integrate Better Auth with Lynx cross-platform framework.
***
title: Lynx Integration
description: Integrate Better Auth with Lynx cross-platform framework.
----------------------------------------------------------------------
This integration guide is for using Better Auth with [Lynx](https://lynxjs.org), a cross-platform rendering framework that enables developers to build applications for Android, iOS, and Web platforms with native rendering performance.
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
## Installation
Install Better Auth and the Lynx React dependency:
npm
pnpm
yarn
bun
```bash
npm install better-auth @lynx-js/react
```
```bash
pnpm add better-auth @lynx-js/react
```
```bash
yarn add better-auth @lynx-js/react
```
```bash
bun add better-auth @lynx-js/react
```
## Create Client Instance
Import `createAuthClient` from `better-auth/lynx` to create your client instance:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/lynx"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000" // The base URL of your auth server
})
```
## Usage
The Lynx client provides the same API as other Better Auth clients, with optimized integration for Lynx's reactive system.
### Authentication Methods
```ts
import { authClient } from "./lib/auth-client"
// Sign in with email and password
await authClient.signIn.email({
email: "test@user.com",
password: "password1234"
})
// Sign up
await authClient.signUp.email({
email: "test@user.com",
password: "password1234",
name: "John Doe"
})
// Sign out
await authClient.signOut()
```
### Hooks
The Lynx client includes reactive hooks that integrate seamlessly with Lynx's component system:
#### useSession
```tsx title="components/user.tsx"
import { authClient } from "../lib/auth-client"
export function User() {
const {
data: session,
isPending, // loading state
error // error object
} = authClient.useSession()
if (isPending) return
Loading...
if (error) return
Error: {error.message}
return (
{session ? (
Welcome, {session.user.name}!
) : (
)}
)
}
```
### Store Integration
The Lynx client uses [nanostores](https://github.com/nanostores/nanostores) for state management and provides a `useStore` hook for accessing reactive state:
```tsx title="components/session-info.tsx"
import { useStore } from "better-auth/lynx"
import { authClient } from "../lib/auth-client"
export function SessionInfo() {
// Access the session store directly
const session = useStore(authClient.$store.session)
return (
{session && (
{JSON.stringify(session, null, 2)}
)}
)
}
```
### Advanced Store Usage
You can use the store with selective key watching for optimized re-renders:
```tsx title="components/optimized-user.tsx"
import { useStore } from "better-auth/lynx"
import { authClient } from "../lib/auth-client"
export function OptimizedUser() {
// Only re-render when specific keys change
const session = useStore(authClient.$store.session, {
keys: ['user.name', 'user.email'] // Only watch these specific keys
})
return (
{session?.user && (
{session.user.name}
{session.user.email}
)}
)
}
```
## Plugin Support
The Lynx client supports all Better Auth plugins:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/lynx"
import { magicLinkClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [
magicLinkClient()
]
})
// Use plugin methods
await authClient.signIn.magicLink({
email: "test@email.com"
})
```
## Error Handling
Error handling works the same as other Better Auth clients:
```tsx title="components/login-form.tsx"
import { authClient } from "../lib/auth-client"
export function LoginForm() {
const signIn = async (email: string, password: string) => {
const { data, error } = await authClient.signIn.email({
email,
password
})
if (error) {
console.error('Login failed:', error.message)
return
}
console.log('Login successful:', data)
}
return (
)
}
```
## Features
The Lynx client provides:
* **Cross-Platform Support**: Works across Android, iOS, and Web platforms
* **Optimized Performance**: Built specifically for Lynx's reactive system
* **Nanostores Integration**: Uses nanostores for efficient state management
* **Selective Re-rendering**: Watch specific store keys to minimize unnecessary updates
* **Full API Compatibility**: All Better Auth methods and plugins work seamlessly
* **TypeScript Support**: Full type safety with TypeScript inference
The Lynx integration maintains all the features and benefits of Better Auth while providing optimal performance and developer experience within Lynx's cross-platform ecosystem.
# integrations: NestJS Integration
URL: /docs/integrations/nestjs
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/nestjs.mdx
Integrate Better Auth with NestJS.
***
title: NestJS Integration
description: Integrate Better Auth with NestJS.
-----------------------------------------------
This guide will show you how to integrate Better Auth with [NestJS](https://nestjs.com/).
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
The NestJS integration is **community maintained**. If you encounter any issues, please open them at [nestjs-better-auth](https://github.com/ThallesP/nestjs-better-auth).
## Installation
Install the NestJS integration library:
npm
pnpm
yarn
bun
```bash
npm install @thallesp/nestjs-better-auth
```
```bash
pnpm add @thallesp/nestjs-better-auth
```
```bash
yarn add @thallesp/nestjs-better-auth
```
```bash
bun add @thallesp/nestjs-better-auth
```
## Basic Setup
Currently, Better Auth's NestJS integration **only supports Express** and does not work with Fastify.
### 1. Disable Body Parser
Disable NestJS's built-in body parser to allow Better Auth to handle the raw request body:
```ts title="main.ts"
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bodyParser: false, // Required for Better Auth
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
```
### 2. Import AuthModule
Import the `AuthModule` in your root module:
```ts title="app.module.ts"
import { Module } from '@nestjs/common';
import { AuthModule } from '@thallesp/nestjs-better-auth';
import { auth } from "./auth"; // Your Better Auth instance
@Module({
imports: [
AuthModule.forRoot(auth),
],
})
export class AppModule {}
```
### 3. Protect Routes
Use the `AuthGuard` to protect your routes:
```ts title="user.controller.ts"
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard, Session, UserSession } from '@thallesp/nestjs-better-auth';
@Controller('users')
@UseGuards(AuthGuard)
export class UserController {
@Get('me')
async getProfile(@Session() session: UserSession) {
return { user: session.user };
}
}
```
## Full Documentation
For comprehensive documentation including decorators, hooks, global guards, and advanced configuration, visit the [NestJS Better Auth repository](https://github.com/thallesp/nestjs-better-auth).
# integrations: Next.js integration
URL: /docs/integrations/next
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/next.mdx
Integrate Better Auth with Next.js.
***
title: Next.js integration
description: Integrate Better Auth with Next.js.
------------------------------------------------
Better Auth can be easily integrated with Next.js. Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Create API Route
We need to mount the handler to an API route. Create a route file inside `/api/auth/[...all]` directory. And add the following code:
```ts title="api/auth/[...all]/route.ts"
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
```
You can change the path on your better-auth configuration but it's recommended to keep it as `/api/auth/[...all]`
For `pages` route, you need to use `toNodeHandler` instead of `toNextJsHandler` and set `bodyParser` to `false` in the `config` object. Here is an example:
```ts title="pages/api/auth/[...all].ts"
import { toNodeHandler } from "better-auth/node"
import { auth } from "@/lib/auth"
// Disallow body parsing, we will parse it manually
export const config = { api: { bodyParser: false } }
export default toNodeHandler(auth.handler)
```
## Create a client
Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react
export const authClient = createAuthClient({
//you can pass client configuration here
})
```
Once you have created the client, you can use it to sign up, sign in, and perform other actions.
Some of the actions are reactive. The client uses [nano-store](https://github.com/nanostores/nanostores) to store the state and re-render the components when the state changes.
The client also uses [better-fetch](https://github.com/bekacru/better-fetch) to make the requests. You can pass the fetch configuration to the client.
## RSC and Server actions
The `api` object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside Better Auth is a invocable as a function. Including plugins endpoints.
**Example: Getting Session on a server action**
```tsx title="server.ts"
import { auth } from "@/lib/auth"
import { headers } from "next/headers"
const someAuthenticatedAction = async () => {
"use server";
const session = await auth.api.getSession({
headers: await headers()
})
};
```
**Example: Getting Session on a RSC**
```tsx
import { auth } from "@/lib/auth"
import { headers } from "next/headers"
export async function ServerComponent() {
const session = await auth.api.getSession({
headers: await headers()
})
if(!session) {
return
Not authenticated
}
return (
Welcome {session.user.name}
)
}
```
As RSCs cannot set cookies, the [cookie cache](/docs/concepts/session-management#cookie-cache) will not be refreshed until the server is interacted with from the client via Server Actions or Route Handlers.
### Server Action Cookies
When you call a function that needs to set cookies, like `signInEmail` or `signUpEmail` in a server action, cookies won’t be set. This is because server actions need to use the `cookies` helper from Next.js to set cookies.
To simplify this, you can use the `nextCookies` plugin, which will automatically set cookies for you whenever a `Set-Cookie` header is present in the response.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
//...your config
plugins: [nextCookies()] // make sure this is the last plugin in the array // [!code highlight]
})
```
Now, when you call functions that set cookies, they will be automatically set.
```ts
"use server";
import { auth } from "@/lib/auth"
const signIn = async () => {
await auth.api.signInEmail({
body: {
email: "user@email.com",
password: "password",
}
})
}
```
## Middleware
In Next.js middleware, it's recommended to only check for the existence of a session cookie to handle redirection. To avoid blocking requests by making API or database calls.
You can use the `getSessionCookie` helper from Better Auth for this purpose:
The getSessionCookie() function does not automatically reference the auth config specified in auth.ts. Therefore, if you customized the cookie name or prefix, you need to ensure that the configuration in getSessionCookie() matches the config defined in your auth.ts.
```ts
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
// THIS IS NOT SECURE!
// This is the recommended approach to optimistically redirect users
// We recommend handling auth checks in each page/route
if (!sessionCookie) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard"], // Specify the routes the middleware applies to
};
```
**Security Warning:** The `getSessionCookie` function only checks for the
existence of a session cookie; it does **not** validate it. Relying solely
on this check for security is dangerous, as anyone can manually create a
cookie to bypass it. You must always validate the session on your server for
any protected actions or pages.
If you have a custom cookie name or prefix, you can pass it to the `getSessionCookie` function.
```ts
const sessionCookie = getSessionCookie(request, {
cookieName: "my_session_cookie",
cookiePrefix: "my_prefix"
});
```
Alternatively, you can use the `getCookieCache` helper to get the session object from the cookie cache.
```ts
import { getCookieCache } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const session = await getCookieCache(request);
if (!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
```
### How to handle auth checks in each page/route
In this example, we are using the `auth.api.getSession` function within a server component to get the session object,
then we are checking if the session is valid. If it's not, we are redirecting the user to the sign-in page.
```tsx title="app/dashboard/page.tsx"
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers()
})
if(!session) {
redirect("/sign-in")
}
return (
Welcome {session.user.name}
)
}
```
### For Next.js release `15.1.7` and below
If you need the full session object, you'll have to fetch it from the `/get-session` API route. Since Next.js middleware doesn't support running Node.js APIs directly, you must make an HTTP request.
The example uses [better-fetch](https://better-fetch.vercel.app), but you can use any fetch library.
```ts
import { betterFetch } from "@better-fetch/fetch";
import type { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
type Session = typeof auth.$Infer.Session;
export async function middleware(request: NextRequest) {
const { data: session } = await betterFetch("/api/auth/get-session", {
baseURL: request.nextUrl.origin,
headers: {
cookie: request.headers.get("cookie") || "", // Forward the cookies from the request
},
});
if (!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard"], // Apply middleware to specific routes
};
```
### For Next.js release `15.2.0` and above
From the version 15.2.0, Next.js allows you to use the `Node.js` runtime in middleware. This means you can use the `auth.api` object directly in middleware.
You may refer to the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware#runtime) for more information about runtime configuration, and how to enable it.
Be careful when using the new runtime. It's an experimental feature and it may be subject to breaking changes.
```ts
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: await headers()
})
if(!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
runtime: "nodejs",
matcher: ["/dashboard"], // Apply middleware to specific routes
};
```
# integrations: Nitro Integration
URL: /docs/integrations/nitro
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/nitro.mdx
Integrate Better Auth with Nitro.
***
title: Nitro Integration
description: Integrate Better Auth with Nitro.
----------------------------------------------
Better Auth can be integrated with your [Nitro Application](https://nitro.build/) (an open source framework to build web servers).
This guide aims to help you integrate Better Auth with your Nitro application in a few simple steps.
## Create a new Nitro Application
Start by scaffolding a new Nitro application using the following command:
```bash title="Terminal"
npx giget@latest nitro nitro-app --install
```
This will create the `nitro-app` directory and install all the dependencies. You can now open the `nitro-app` directory in your code editor.
### Prisma Adapter Setup
This guide assumes that you have a basic understanding of Prisma. If you are new to Prisma, you can check out the [Prisma documentation](https://www.prisma.io/docs/getting-started).
The `sqlite` database used in this guide will not work in a production environment. You should replace it with a production-ready database like `PostgreSQL`.
For this guide, we will be using the Prisma adapter. You can install prisma client by running the following command:
npm
pnpm
yarn
bun
```bash
npm install @prisma/client
```
```bash
pnpm add @prisma/client
```
```bash
yarn add @prisma/client
```
```bash
bun add @prisma/client
```
`prisma` can be installed as a dev dependency using the following command:
npm
pnpm
yarn
bun
```bash
npm install -D prisma
```
```bash
pnpm add -D prisma
```
```bash
yarn add --dev prisma
```
```bash
bun add --dev prisma
```
Generate a `schema.prisma` file in the `prisma` directory by running the following command:
```bash title="Terminal"
npx prisma init
```
You can now replace the contents of the `schema.prisma` file with the following:
```prisma title="prisma/schema.prisma"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// Will be deleted. Just need it to generate the prisma client
model Test {
id Int @id @default(autoincrement())
name String
}
```
Ensure that you update the `DATABASE_URL` in your `.env` file to point to the location of your database.
```txt title=".env"
DATABASE_URL="file:./dev.db"
```
Run the following command to generate the Prisma client & sync the database:
```bash title="Terminal"
npx prisma db push
```
### Install & Configure Better Auth
Follow steps 1 & 2 from the [installation guide](/docs/installation) to install Better Auth in your Nitro application & set up the environment variables.
Once that is done, create your Better Auth instance within the `server/utils/auth.ts` file.
```ts title="server/utils/auth.ts"
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, { provider: "sqlite" }),
emailAndPassword: { enabled: true },
});
```
### Update Prisma Schema
Use the Better Auth CLI to update your Prisma schema with the required models by running the following command:
```bash title="Terminal"
npx @better-auth/cli generate --config server/utils/auth.ts
```
The `--config` flag is used to specify the path to the file where you have created your Better Auth instance.
Head over to the `prisma/schema.prisma` file & save the file to trigger the format on save.
After saving the file, you can run the `npx prisma db push` command to update the database schema.
## Mount The Handler
You can now mount the Better Auth handler in your Nitro application. You can do this by adding the following code to your `server/routes/api/auth/[...all].ts` file:
```ts title="server/routes/api/auth/[...all].ts"
export default defineEventHandler((event) => {
return auth.handler(toWebRequest(event));
});
```
This is a [catch-all](https://nitro.build/guide/routing#catch-all-route) route that will handle all requests to `/api/auth/*`.
### CORS
You can configure CORS for your Nitro app by creating a plugin.
Start by installing the cors package:
npm
pnpm
yarn
bun
```bash
npm install cors
```
```bash
pnpm add cors
```
```bash
yarn add cors
```
```bash
bun add cors
```
You can now create a new file `server/plugins/cors.ts` and add the following code:
```ts title="server/plugins/cors.ts"
import cors from "cors";
export default defineNitroPlugin((plugin) => {
plugin.h3App.use(
fromNodeMiddleware(
cors({
origin: "*",
}),
),
);
});
```
This will enable CORS for all routes. You can customize the `origin` property to allow requests from specific domains. Ensure that the config is in sync with your frontend application.
### Auth Guard/Middleware
You can add an auth guard to your Nitro application to protect routes that require authentication. You can do this by creating a new file `server/utils/require-auth.ts` and adding the following code:
```ts title="server/utils/require-auth.ts"
import { EventHandler, H3Event } from "h3";
import { fromNodeHeaders } from "better-auth/node";
/**
* Middleware used to require authentication for a route.
*
* Can be extended to check for specific roles or permissions.
*/
export const requireAuth: EventHandler = async (event: H3Event) => {
const headers = event.headers;
const session = await auth.api.getSession({
headers: headers,
});
if (!session)
throw createError({
statusCode: 401,
statusMessage: "Unauthorized",
});
// You can save the session to the event context for later use
event.context.auth = session;
};
```
You can now use this event handler/middleware in your routes to protect them:
```ts title="server/routes/api/secret.get.ts"
// Object syntax of the route handler
export default defineEventHandler({
// The user has to be logged in to access this route
onRequest: [requireAuth],
handler: async (event) => {
setResponseStatus(event, 201, "Secret data");
return { message: "Secret data" };
},
});
```
### Example
You can find an example of a Nitro application integrated with Better Auth & Prisma [here](https://github.com/BayBreezy/nitrojs-better-auth-prisma).
# integrations: Nuxt Integration
URL: /docs/integrations/nuxt
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/nuxt.mdx
Integrate Better Auth with Nuxt.
***
title: Nuxt Integration
description: Integrate Better Auth with Nuxt.
---------------------------------------------
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Create API Route
We need to mount the handler to an API route. Create a file inside `/server/api/auth` called `[...all].ts` and add the following code:
```ts title="server/api/auth/[...all].ts"
import { auth } from "~/lib/auth"; // import your auth config
export default defineEventHandler((event) => {
return auth.handler(toWebRequest(event));
});
```
You can change the path on your better-auth configuration but it's recommended to keep it as `/api/auth/[...all]`
### Migrate the database
Run the following command to create the necessary tables in your database:
```bash
npx @better-auth/cli migrate
```
## Create a client
Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/vue" // make sure to import from better-auth/vue
export const authClient = createAuthClient({
//you can pass client configuration here
})
```
Once you have created the client, you can use it to sign up, sign in, and perform other actions.
Some of the actions are reactive.
### Example usage
```vue title="index.vue"
{{ session.data }}
```
### Server Usage
The `api` object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside Better Auth is a invocable as a function. Including plugins endpoints.
**Example: Getting Session on a server API route**
```tsx title="server/api/example.ts"
import { auth } from "~/lib/auth";
export default defineEventHandler((event) => {
const session = await auth.api.getSession({
headers: event.headers
});
if(session) {
// access the session.session && session.user
}
});
```
### SSR Usage
If you are using Nuxt with SSR, you can use the `useSession` function in the `setup` function of your page component and pass `useFetch` to make it work with SSR.
```vue title="index.vue"
{{ session }}
```
### Middleware
To add middleware to your Nuxt project, you can use the `useSession` method from the client.
```ts title="middleware/auth.global.ts"
import { authClient } from "~/lib/auth-client";
export default defineNuxtRouteMiddleware(async (to, from) => {
const { data: session } = await authClient.useSession(useFetch);
if (!session.value) {
if (to.path === "/dashboard") {
return navigateTo("/");
}
}
});
```
### Resources & Examples
* [Nuxt and Nuxt Hub example](https://github.com/atinux/nuxthub-better-auth) on GitHub.
* [NuxtZzle is Nuxt,Drizzle ORM example](https://github.com/leamsigc/nuxt-better-auth-drizzle) on GitHub [preview](https://nuxt-better-auth.giessen.dev/)
* [Nuxt example](https://stackblitz.com/github/better-auth/better-auth/tree/main/examples/nuxt-example) on StackBlitz.
* [NuxSaaS (Github)](https://github.com/NuxSaaS/NuxSaaS) is a full-stack SaaS Starter Kit that leverages Better Auth for secure and efficient user authentication. [Demo](https://nuxsaas.com/)
* [NuxtOne (Github)](https://github.com/nuxtone/nuxt-one) is a Nuxt-based starter template for building AIaaS (AI-as-a-Service) applications [preview](https://www.one.devv.zone)
# integrations: Remix Integration
URL: /docs/integrations/remix
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/remix.mdx
Integrate Better Auth with Remix.
***
title: Remix Integration
description: Integrate Better Auth with Remix.
----------------------------------------------
Better Auth can be easily integrated with Remix. This guide will show you how to integrate Better Auth with Remix.
You can follow the steps from [installation](/docs/installation) to get started or you can follow this guide to make it the Remix-way.
If you have followed the installation steps, you can skip the first step.
## Create auth instance
Create a file named `auth.server.ts` in one of these locations:
* Project root
* `lib/` folder
* `utils/` folder
You can also nest any of these folders under `app/` folder. (e.g. `app/lib/auth.server.ts`)
And in this file, import Better Auth and create your instance.
Make sure to export the auth instance with the variable name `auth` or as a `default` export.
```ts title="app/lib/auth.server.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
database: {
provider: "postgres", //change this to your database provider
url: process.env.DATABASE_URL, // path to your database or connection string
}
})
```
## Create API Route
We need to mount the handler to a API route. Create a resource route file `api.auth.$.ts` inside `app/routes/` directory. And add the following code:
```ts title="app/routes/api.auth.$.ts"
import { auth } from '~/lib/auth.server' // Adjust the path as necessary
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"
export async function loader({ request }: LoaderFunctionArgs) {
return auth.handler(request)
}
export async function action({ request }: ActionFunctionArgs) {
return auth.handler(request)
}
```
You can change the path on your better-auth configuration but it's recommended to keep it as `routes/api.auth.$.ts`
## Create a client
Create a client instance. Here we are creating `auth-client.ts` file inside the `lib/` directory.
```ts title="app/lib/auth-client.ts"
import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react
export const authClient = createAuthClient({
//you can pass client configuration here
})
```
Once you have created the client, you can use it to sign up, sign in, and perform other actions.
### Example usage
#### Sign Up
```ts title="app/routes/signup.tsx"
import { Form } from "@remix-run/react"
import { useState } from "react"
import { authClient } from "~/lib/auth-client"
export default function SignUp() {
const [email, setEmail] = useState("")
const [name, setName] = useState("")
const [password, setPassword] = useState("")
const signUp = async () => {
await authClient.signUp.email(
{
email,
password,
name,
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
// redirect to home
},
onError: (ctx) => {
alert(ctx.error)
},
},
)
}
return (
Sign Up
)
}
```
#### Sign In
```ts title="app/routes/signin.tsx"
import { Form } from "@remix-run/react"
import { useState } from "react"
import { authClient } from "~/services/auth-client"
export default function SignIn() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const signIn = async () => {
await authClient.signIn.email(
{
email,
password,
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
// redirect to home
},
onError: (ctx) => {
alert(ctx.error)
},
},
)
}
return (
Sign In
)
}
```
# integrations: SolidStart Integration
URL: /docs/integrations/solid-start
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/solid-start.mdx
Integrate Better Auth with SolidStart.
***
title: SolidStart Integration
description: Integrate Better Auth with SolidStart.
---------------------------------------------------
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
We need to mount the handler to SolidStart server. Put the following code in your `*auth.ts` file inside `/routes/api/auth` folder.
```ts title="*auth.ts"
import { auth } from "~/lib/auth";
import { toSolidStartHandler } from "better-auth/solid-start";
export const { GET, POST } = toSolidStartHandler(auth);
```
# integrations: SvelteKit Integration
URL: /docs/integrations/svelte-kit
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/svelte-kit.mdx
Integrate Better Auth with SvelteKit.
***
title: SvelteKit Integration
description: Integrate Better Auth with SvelteKit.
--------------------------------------------------
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
We need to mount the handler to SvelteKit server hook.
```ts title="hooks.server.ts"
import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
import { building } from "$app/environment";
export async function handle({ event, resolve }) {
return svelteKitHandler({ event, resolve, auth, building });
}
```
### Populate session data in the event (`event.locals`)
The `svelteKitHandler` does not automatically populate `event.locals.user` or `event.locals.session`. If you want to access the current session in your server code (e.g., in `+layout.server.ts`, actions, or endpoints), populate `event.locals` in your `handle` hook:
```ts title="hooks.server.ts"
import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
import { building } from "$app/environment";
export async function handle({ event, resolve }) {
// Fetch current session from Better Auth
const session = await auth.api.getSession({
headers: event.request.headers,
});
// Make session and user available on server
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
}
return svelteKitHandler({ event, resolve, auth, building });
}
```
### Server Action Cookies
To ensure cookies are properly set when you call functions like `signInEmail` or `signUpEmail` in a server action, you should use the `sveltekitCookies` plugin. This plugin will automatically handle setting cookies for you in SvelteKit.
You need to add it as a plugin to your Better Auth instance.
The `getRequestEvent` function is available in SvelteKit `2.20.0` and later.
Make sure you are using a compatible version.
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth";
import { sveltekitCookies } from "better-auth/svelte-kit";
import { getRequestEvent } from "$app/server";
export const auth = betterAuth({
// ... your config
plugins: [sveltekitCookies(getRequestEvent)], // make sure this is the last plugin in the array
});
```
## Create a client
Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/svelte"; // make sure to import from better-auth/svelte
export const authClient = createAuthClient({
// you can pass client configuration here
});
```
Once you have created the client, you can use it to sign up, sign in, and perform other actions.
Some of the actions are reactive. The client use [nano-store](https://github.com/nanostores/nanostores) to store the state and reflect changes when there is a change like a user signing in or out affecting the session state.
### Example usage
```svelte
{#if $session.data}
{$session?.data?.user.name}
{:else}
{/if}
```
# integrations: TanStack Start Integration
URL: /docs/integrations/tanstack
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/integrations/tanstack.mdx
Integrate Better Auth with TanStack Start.
***
title: TanStack Start Integration
description: Integrate Better Auth with TanStack Start.
-------------------------------------------------------
This integration guide is assuming you are using TanStack Start.
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
We need to mount the handler to a TanStack API endpoint/Server Route.
Create a new file: `/src/routes/api/auth/$.ts`
```ts title="src/routes/api/auth/$.ts"
import { auth } from '@/lib/auth' // import your auth instance
import { createServerFileRoute } from '@tanstack/react-start/server'
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
GET: ({ request }) => {
return auth.handler(request)
},
POST: ({ request }) => {
return auth.handler(request)
},
})
```
If you haven't created your server route handler yet, you can do so by creating a file: `/src/server.ts`
```ts title="src/server.ts"
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createRouter } from './router'
export default createStartHandler({
createRouter,
})(defaultStreamHandler)
```
### Usage tips
* We recommend using the client SDK or `authClient` to handle authentication, rather than server actions with `auth.api`.
* When you call functions that need to set cookies (like `signInEmail` or `signUpEmail`), you'll need to handle cookie setting for TanStack Start. Better Auth provides a `reactStartCookies` plugin to automatically handle this for you.
```ts title="src/lib/auth.ts"
import { betterAuth } from "better-auth";
import { reactStartCookies } from "better-auth/react-start";
export const auth = betterAuth({
//...your config
plugins: [reactStartCookies()] // make sure this is the last plugin in the array
})
```
Now, when you call functions that set cookies, they will be automatically set using TanStack Start's cookie handling system.
```ts
import { auth } from "@/lib/auth"
const signIn = async () => {
await auth.api.signInEmail({
body: {
email: "user@email.com",
password: "password",
}
})
}
```
# reference: Contributing to BetterAuth
URL: /docs/reference/contributing
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/reference/contributing.mdx
A concise guide to contributing to BetterAuth
***
title: Contributing to BetterAuth
description: A concise guide to contributing to BetterAuth
----------------------------------------------------------
Thank you for your interest in contributing to Better Auth! This guide is a concise guide to contributing to Better Auth.
## Getting Started
Before diving in, here are a few important resources:
* Take a look at our existing issues and pull requests
* Join our community discussions in Discord
## Development Setup
To get started with development:
Make sure you have Node.JS{" "}
installed, preferably on LTS.
### 1. Fork the repository
Visit [https://github.com/better-auth/better-auth](https://github.com/better-auth/better-auth)
Click the "Fork" button in the top right.
### 2. Clone your fork
```bash
# Replace YOUR-USERNAME with your GitHub username
git clone https://github.com/YOUR-USERNAME/better-auth.git
cd better-auth
```
### 3. Install dependencies
Make sure you have pnpm installed!
```bash
pnpm install
```
### 4. Prepare ENV files
Copy the example env file to create your new `.env` file.
```bash
cp -n ./docs/.env.example ./docs/.env
```
## Making changes
Once you have an idea of what you want to contribute, you can start making changes. Here are some steps to get started:
### 1. Create a new branch
```bash
# Make sure you're on main
git checkout main
# Pull latest changes
git pull upstream main
# Create and switch to a new branch
git checkout -b feature/your-feature-name
```
### 2. Start development server
Start the development server:
```bash
pnpm dev
```
To start the docs server:
```bash
pnpm -F docs dev
```
### 3. Make Your Changes
* Make your changes to the codebase.
* Write tests if needed. (Read more about testing here)
* Update documentation. (Read more about documenting here)
### Issues and Bug Fixes
* Check our [GitHub issues](https://github.com/better-auth/better-auth/issues) for tasks labeled `good first issue`
* When reporting bugs, include steps to reproduce and expected behavior
* Comment on issues you'd like to work on to avoid duplicate efforts
### Framework Integrations
We welcome contributions to support more frameworks:
* Focus on framework-agnostic solutions where possible
* Keep integrations minimal and maintainable
* All integrations currently live in the main package
### Plugin Development
* For core plugins: Open an issue first to discuss your idea
* For community plugins: Feel free to develop independently
* Follow our plugin architecture guidelines
### Documentation
* Fix typos and errors
* Add examples and clarify existing content
* Ensure documentation is up to date with code changes
## Testing
We use Vitest for testing. Place test files next to the source files they test:
```ts
import { describe, it, expect } from "vitest";
import { getTestInstance } from "./test-utils/test-instance";
describe("Feature", () => {
it("should work as expected", async () => {
const { client } = getTestInstance();
// Test code here
expect(result).toBeDefined();
});
});
```
### Testing Best Practices
* Write clear commit messages
* Update documentation to reflect your changes
* Add tests for new features
* Follow our coding standards
* Keep pull requests focused on a single change
## Need Help?
Don't hesitate to ask for help! You can:
* Open an issue with questions
* Join our community discussions
* Reach out to project maintainers
Thank you for contributing to Better Auth!
# reference: FAQ
URL: /docs/reference/faq
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/reference/faq.mdx
Frequently asked questions about Better Auth.
***
title: FAQ
description: Frequently asked questions about Better Auth.
----------------------------------------------------------
This page contains frequently asked questions, common issues, and other helpful information about Better Auth.
When encountering `createAuthClient` related errors, make sure to have the correct import path as it varies based on environment.
If you're using the auth client on react front-end, you'll need to import it from `/react`:
```ts title="component.ts"
import { createAuthClient } from "better-auth/react";
```
Where as if you're using the auth client in Next.js middleware, server-actions, server-components or anything server-related, you'll likely need to import it from `/client`:
```ts title="server.ts"
import { createAuthClient } from "better-auth/client";
```
If you try to call `authClient.getSession` on a server environment (e.g, a Next.js server component), it doesn't work since it can't access the cookies. You can use the `auth.api.getSession` instead and pass the request headers to it.
```tsx title="server.tsx"
import { auth } from "./auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({
headers: await headers()
})
```
if you need to use the auth client on the server for different purposes, you still can pass the request headers to it:
```tsx title="server.tsx"
import { authClient } from "./auth-client";
import { headers } from "next/headers";
const session = await authClient.getSession({
fetchOptions:{
headers: await headers()
}
})
```
Better Auth provides a type-safe way to extend the user and session schemas, take a look at our docs on extending core schema.
Both `useSession` and `getSession` instances are used fundamentally different based on the situation.
`useSession` is a hook, meaning it can trigger re-renders whenever session data changes.
If you have UI you need to change based on user or session data, you can use this hook.
For performance reasons, do not use this hook on your `layout.tsx` file. We
recommend using RSC and use your server auth instance to get the session data
via `auth.api.getSession`.
`getSession` returns a promise containing data and error.
For all other situations where you shouldn't use `useSession`, is when you should be using `getSession`.
`getSession` is available on both server and client auth instances.
Not just the latter.
If you're facing typescript errors, make sure your tsconfig has `strict` set to `true`:
```json title="tsconfig.json"
{
"compilerOptions": {
"strict": true,
}
}
```
if you can't set strict to true, you can enable strictNullChecks:
```json title="tsconfig.json"
{
"compilerOptions": {
"strictNullChecks": true,
}
}
```
You can learn more in our TypeScript docs.
At this time, you can't remove the `name`, `image`, or `email` fields from the user table.
We do plan to have more customizability in the future in this regard, but for now, you can't remove these fields.
# reference: Options
URL: /docs/reference/options
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/reference/options.mdx
Better Auth configuration options reference.
***
title: Options
description: Better Auth configuration options reference.
---------------------------------------------------------
List of all the available options for configuring Better Auth. See [Better Auth Options](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/types/options.ts#L13).
## `appName`
The name of the application.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
appName: "My App",
})
```
## `baseURL`
Base URL for Better Auth. This is typically the root URL where your application server is hosted. Note: If you include a path in the baseURL, it will take precedence over the default path.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
baseURL: "https://example.com",
})
```
If not explicitly set, the system will check for the environment variable `process.env.BETTER_AUTH_URL`
## `basePath`
Base path for Better Auth. This is typically the path where the Better Auth routes are mounted. It will be overridden if there is a path component within `baseURL`.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
basePath: "/api/auth",
})
```
Default: `/api/auth`
## `trustedOrigins`
List of trusted origins. You can provide a static array of origins, a function that returns origins dynamically, or use wildcard patterns to match multiple domains.
### Static Origins
You can provide a static array of origins:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
trustedOrigins: ["http://localhost:3000", "https://example.com"],
})
```
### Dynamic Origins
You can provide a function that returns origins dynamically:
```ts
export const auth = betterAuth({
trustedOrigins: async (request: Request) => {
// Return an array of trusted origins based on the request
return ["https://dynamic-origin.com"];
}
})
```
### Wildcard Support
You can use wildcard patterns in trusted origins:
```ts
export const auth = betterAuth({
trustedOrigins: [
"*.example.com", // Trust all subdomains of example.com
"https://*.example.com", // Trust only HTTPS subdomains
"http://*.dev.example.com" // Trust HTTP subdomains of dev.example.com
]
})
```
## `secret`
The secret used for encryption, signing, and hashing.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
secret: "your-secret-key",
})
```
By default, Better Auth will look for the following environment variables:
* `process.env.BETTER_AUTH_SECRET`
* `process.env.AUTH_SECRET`
If none of these environment variables are set, it will default to `"better-auth-secret-123456789"`. In production, if it's not set, it will throw an error.
You can generate a good secret using the following command:
```bash
openssl rand -base64 32
```
## `database`
Database configuration for Better Auth.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
dialect: "postgres",
type: "postgres",
casing: "camel"
},
})
```
Better Auth supports various database configurations including [PostgreSQL](/docs/adapters/postgresql), [MySQL](/docs/adapters/mysql), and [SQLite](/docs/adapters/sqlite).
Read more about databases [here](/docs/concepts/database).
## `secondaryStorage`
Secondary storage configuration used to store session and rate limit data.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
// ... other options
secondaryStorage: {
// Your implementation here
},
})
```
Read more about secondary storage [here](/docs/concepts/database#secondary-storage).
## `emailVerification`
Email verification configuration.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ({ user, url, token }) => {
// Send verification email to user
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 3600 // 1 hour
},
})
```
* `sendVerificationEmail`: Function to send verification email
* `sendOnSignUp`: Send verification email automatically after sign up (default: `false`)
* `sendOnSignIn`: Send verification email automatically on sign in when the user's email is not verified (default: `false`)
* `autoSignInAfterVerification`: Auto sign in the user after they verify their email
* `expiresIn`: Number of seconds the verification token is valid for (default: `3600` seconds)
## `emailAndPassword`
Email and password authentication configuration.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
disableSignUp: false,
requireEmailVerification: true,
minPasswordLength: 8,
maxPasswordLength: 128,
autoSignIn: true,
sendResetPassword: async ({ user, url, token }) => {
// Send reset password email
},
resetPasswordTokenExpiresIn: 3600, // 1 hour
password: {
hash: async (password) => {
// Custom password hashing
return hashedPassword;
},
verify: async ({ hash, password }) => {
// Custom password verification
return isValid;
}
}
},
})
```
* `enabled`: Enable email and password authentication (default: `false`)
* `disableSignUp`: Disable email and password sign up (default: `false`)
* `requireEmailVerification`: Require email verification before a session can be created
* `minPasswordLength`: Minimum password length (default: `8`)
* `maxPasswordLength`: Maximum password length (default: `128`)
* `autoSignIn`: Automatically sign in the user after sign up
* `sendResetPassword`: Function to send reset password email
* `resetPasswordTokenExpiresIn`: Number of seconds the reset password token is valid for (default: `3600` seconds)
* `password`: Custom password hashing and verification functions
## `socialProviders`
Configure social login providers.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectUri: "https://example.com/api/auth/callback/google"
},
github: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectUri: "https://example.com/api/auth/callback/github"
}
},
})
```
## `plugins`
List of Better Auth plugins.
```ts
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
emailOTP({
sendVerificationOTP: async ({ email, otp, type }) => {
// Send OTP to user's email
}
})
],
})
```
## `user`
User configuration options.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
user: {
modelName: "users",
fields: {
email: "emailAddress",
name: "fullName"
},
additionalFields: {
customField: {
type: "string",
}
},
changeEmail: {
enabled: true,
sendChangeEmailVerification: async ({ user, newEmail, url, token }) => {
// Send change email verification
}
},
deleteUser: {
enabled: true,
sendDeleteAccountVerification: async ({ user, url, token }) => {
// Send delete account verification
},
beforeDelete: async (user) => {
// Perform actions before user deletion
},
afterDelete: async (user) => {
// Perform cleanup after user deletion
}
}
},
})
```
* `modelName`: The model name for the user (default: `"user"`)
* `fields`: Map fields to different column names
* `additionalFields`: Additional fields for the user table
* `changeEmail`: Configuration for changing email
* `deleteUser`: Configuration for user deletion
## `session`
Session configuration options.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
modelName: "sessions",
fields: {
userId: "user_id"
},
expiresIn: 604800, // 7 days
updateAge: 86400, // 1 day
disableSessionRefresh: true, // Disable session refresh so that the session is not updated regardless of the `updateAge` option. (default: `false`)
additionalFields: { // Additional fields for the session table
customField: {
type: "string",
}
},
storeSessionInDatabase: true, // Store session in database when secondary storage is provided (default: `false`)
preserveSessionInDatabase: false, // Preserve session records in database when deleted from secondary storage (default: `false`)
cookieCache: {
enabled: true, // Enable caching session in cookie (default: `false`)
maxAge: 300 // 5 minutes
}
},
})
```
* `modelName`: The model name for the session (default: `"session"`)
* `fields`: Map fields to different column names
* `expiresIn`: Expiration time for the session token in seconds (default: `604800` - 7 days)
* `updateAge`: How often the session should be refreshed in seconds (default: `86400` - 1 day)
* `additionalFields`: Additional fields for the session table
* `storeSessionInDatabase`: Store session in database when secondary storage is provided (default: `false`)
* `preserveSessionInDatabase`: Preserve session records in database when deleted from secondary storage (default: `false`)
* `cookieCache`: Enable caching session in cookie
## `account`
Account configuration options.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
modelName: "accounts",
fields: {
userId: "user_id"
},
encryptOAuthTokens: true, // Encrypt OAuth tokens before storing them in the database
accountLinking: {
enabled: true,
trustedProviders: ["google", "github", "email-password"],
allowDifferentEmails: false
}
},
})
```
* `modelName`: The model name for the account
* `fields`: Map fields to different column names
### `encryptOAuthTokens`
Encrypt OAuth tokens before storing them in the database. Default: `false`.
### `updateAccountOnSignIn`
If enabled (true), the user account data (accessToken, idToken, refreshToken, etc.)
will be updated on sign in with the latest data from the provider.
### `accountLinking`
Configuration for account linking.
* `enabled`: Enable account linking (default: `false`)
* `trustedProviders`: List of trusted providers
* `allowDifferentEmails`: Allow users to link accounts with different email addresses
* `allowUnlinkingAll`: Allow users to unlink all accounts
## `verification`
Verification configuration options.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
verification: {
modelName: "verifications",
fields: {
userId: "user_id"
},
disableCleanup: false
},
})
```
* `modelName`: The model name for the verification table
* `fields`: Map fields to different column names
* `disableCleanup`: Disable cleaning up expired values when a verification value is fetched
## `rateLimit`
Rate limiting configuration.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
enabled: true,
window: 10,
max: 100,
customRules: {
"/example/path": {
window: 10,
max: 100
}
},
storage: "memory",
modelName: "rateLimit"
}
})
```
* `enabled`: Enable rate limiting (defaults: `true` in production, `false` in development)
* `window`: Time window to use for rate limiting. The value should be in seconds. (default: `10`)
* `max`: The default maximum number of requests allowed within the window. (default: `100`)
* `customRules`: Custom rate limit rules to apply to specific paths.
* `storage`: Storage configuration. If you passed a secondary storage, rate limiting will be stored in the secondary storage. (options: `"memory", "database", "secondary-storage"`, default: `"memory"`)
* `modelName`: The name of the table to use for rate limiting if database is used as storage. (default: `"rateLimit"`)
## `advanced`
Advanced configuration options.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
advanced: {
ipAddress: {
ipAddressHeaders: ["x-client-ip", "x-forwarded-for"],
disableIpTracking: false
},
useSecureCookies: true,
disableCSRFCheck: false,
crossSubDomainCookies: {
enabled: true,
additionalCookies: ["custom_cookie"],
domain: "example.com"
},
cookies: {
session_token: {
name: "custom_session_token",
attributes: {
httpOnly: true,
secure: true
}
}
},
defaultCookieAttributes: {
httpOnly: true,
secure: true
},
cookiePrefix: "myapp",
database: {
// If your DB is using auto-incrementing IDs, set this to true.
useNumberId: false,
// Use your own custom ID generator, or disable generating IDs as a whole.
generateId: (((options: {
model: LiteralUnion;
size?: number;
}) => {
return "my-super-unique-id";
})) | false,
defaultFindManyLimit: 100,
}
},
})
```
* `ipAddress`: IP address configuration for rate limiting and session tracking
* `useSecureCookies`: Use secure cookies (default: `false`)
* `disableCSRFCheck`: Disable trusted origins check (⚠️ security risk)
* `crossSubDomainCookies`: Configure cookies to be shared across subdomains
* `cookies`: Customize cookie names and attributes
* `defaultCookieAttributes`: Default attributes for all cookies
* `cookiePrefix`: Prefix for cookies
* `generateId`: Function to generate a unique ID for a model
## `logger`
Logger configuration for Better Auth.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
logger: {
disabled: false,
disableColors: false,
level: "error",
log: (level, message, ...args) => {
// Custom logging implementation
console.log(`[${level}] ${message}`, ...args);
}
}
})
```
The logger configuration allows you to customize how Better Auth handles logging. It supports the following options:
* `disabled`: Disable all logging when set to `true` (default: `false`)
* `disableColors`: Disable colors in the default logger implementation (default: determined by the terminal's color support)
* `level`: Set the minimum log level to display. Available levels are:
* `"info"`: Show all logs
* `"warn"`: Show warnings and errors
* `"error"`: Show only errors
* `"debug"`: Show all logs including debug information
* `log`: Custom logging function that receives:
* `level`: The log level (`"info"`, `"warn"`, `"error"`, or `"debug"`)
* `message`: The log message
* `...args`: Additional arguments passed to the logger
Example with custom logging:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
logger: {
level: "info",
log: (level, message, ...args) => {
// Send logs to a custom logging service
myLoggingService.log({
level,
message,
metadata: args,
timestamp: new Date().toISOString()
});
}
}
})
```
## `databaseHooks`
Database lifecycle hooks for core operations.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (user) => {
// Modify user data before creation
return { data: { ...user, customField: "value" } };
},
after: async (user) => {
// Perform actions after user creation
}
},
update: {
before: async (userData) => {
// Modify user data before update
return { data: { ...userData, updatedAt: new Date() } };
},
after: async (user) => {
// Perform actions after user update
}
}
},
session: {
// Session hooks
},
account: {
// Account hooks
},
verification: {
// Verification hooks
}
},
})
```
## `onAPIError`
API error handling configuration.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
onAPIError: {
throw: true,
onError: (error, ctx) => {
// Custom error handling
console.error("Auth error:", error);
},
errorURL: "/auth/error"
},
})
```
* `throw`: Throw an error on API error (default: `false`)
* `onError`: Custom error handler
* `errorURL`: URL to redirect to on error (default: `/api/auth/error`)
## `hooks`
Request lifecycle hooks.
```ts
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
// Execute before processing the request
console.log("Request path:", ctx.path);
}),
after: createAuthMiddleware(async (ctx) => {
// Execute after processing the request
console.log("Response:", ctx.context.returned);
})
},
})
```
For more details and examples, see the [Hooks documentation](/docs/concepts/hooks).
## `disabledPaths`
Disable specific auth paths.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
disabledPaths: ["/sign-up/email", "/sign-in/email"],
})
```
## `telemetry`
Enable or disable Better Auth's telemetry collection. (default: `false`)
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
telemetry: {
enabled: false,
}
})
```
# reference: Resources
URL: /docs/reference/resources
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/reference/resources.mdx
A curated collection of resources to help you learn and master Better Auth.
***
title: Resources
description: A curated collection of resources to help you learn and master Better Auth.
----------------------------------------------------------------------------------------
import { Resource } from "@/components/resource-section";
A curated collection of resources to help you learn and master Better Auth. From blog posts to video tutorials, find everything you need to get started.
## Video tutorials
Theo(t3.gg) explores the current landscape of authentication, discussing trends, challenges, and where the industry is heading.",
href: "https://www.youtube.com/watch?v=lxslnp-ZEMw",
tags: ["trends", "showcase", "review"],
},
{
title: "Last Authentication You Will Ever Need",
description:
"A comprehensive tutorial demonstrating why Better Auth could be the final authentication solution you'll need for your projects.",
href: "https://www.youtube.com/watch?v=hFtufpaMcLM",
tags: ["implementation", "showcase"],
},
{
title: "This Might Be My New Favourite Auth Library",
description:
"developedbyed explores the features and capabilities of Better Auth, explaining why it stands out among authentication libraries.",
href: "https://www.youtube.com/watch?v=Hjs3zM7o7NE",
tags: ["review", "showcase"],
},
{
title: "8 Reasons To Try Better Auth",
description:
"CJ presents 8 compelling reasons why Better Auth is the BEST auth framework he's ever used, demonstrating its superior features and ease of implementation.",
href: "https://www.youtube.com/watch?v=_OApmLmex14",
tags: ["review", "showcase", "implementation"],
},
{
title: "Better Auth is so good that I almost switched programming languages",
description:
"Dreams of Code reviews Better Auth's features that nearly made them switch languages.",
href: "https://www.youtube.com/watch?v=dNY4FKXwTsM",
tags: ["review", "showcase", "implementation"],
},
{
title: "Best authentication framework for next.js",
description:
"A detailed comparison of authentication frameworks for Next.js, highlighting why Better Auth might be your best choice.",
href: "https://www.youtube.com/watch?v=V--T0q9FrEw",
tags: ["nextjs", "comparison"],
},
{
title: "Better-Auth: A First Look",
description:
"An introductory overview and demonstration of Better Auth's core features and capabilities.",
href: "https://www.youtube.com/watch?v=2cQTV6NYxis",
tags: ["implementation", "showcase"],
},
{
title: "Stripe was never so easy (with better auth)",
description: "A tutorial on how to integrate Stripe with Better Auth.",
href: "https://www.youtube.com/watch?v=g-RIrzBEX6M",
tags: ["implementation"],
},
{
title: "Nextjs 15 Authentication Made EASY with Better Auth",
description:
"A practical guide showing how to seamlessly integrate Better Auth with Next.js 15 for robust authentication.",
href: "https://www.youtube.com/watch?v=lxslnp-ZEMw",
tags: ["nextjs", "implementation", "tutorial"],
},
{
title: "Better Auth: Headless Authentication for Your TanStack Start App",
description: "Jack demonstrates how to implement headless authentication in your TanStack Start application using Better Auth, providing a modern approach to auth.",
href: "https://www.youtube.com/watch?v=Atev8Nxpw7c",
tags: ["tanstack", "implementation"],
},
{
title: "Goodbye Clerk, Hello Better Auth – Full Migration Guide!",
description: "A comprehensive guide showing how to migrate your authentication from Clerk to Better Auth, with step-by-step instructions and best practices.",
href: "https://www.youtube.com/watch?v=Za_QihbDSuk",
tags: ["migration", "clerk", "tutorial"],
},
]
}
/>
## Blog posts
# reference: Security
URL: /docs/reference/security
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/reference/security.mdx
Better Auth security features.
***
title: Security
description: Better Auth security features.
-------------------------------------------
This page contains information about security features of Better Auth.
## Password Hashing
Better Auth uses the `scrypt` algorithm to hash passwords by default. This algorithm is designed to be memory-hard and CPU-intensive, making it resistant to brute-force attacks. You can customize the password hashing function by setting the `password` option in the configuration. This option should include a `hash` function to hash passwords and a `verify` function to verify them.
## Session Management
### Session Expiration
Better Auth uses secure session management to protect user data. Sessions are stored in the database or a secondary storage, if configured, to prevent unauthorized access. By default, sessions expire after 7 days, but you can customize this value in the configuration. Additionally, each time a session is used, if it reaches the `updateAge` threshold, the expiration date is extended, which by default is set to 1 day.
### Session Revocation
Better Auth allows you to revoke sessions to enhance security. When a session is revoked, the user is logged out and can no longer access the application. A logged in user can also revoke their own sessions to log out from different devices or browsers.
See the [session management](/docs/concepts/session-management) for more details.
## CSRF Protection
Better Auth ensures CSRF protection by validating the Origin header in requests. This check confirms that requests originate from the application or a trusted source. If a request comes from an untrusted origin, it is blocked to prevent potential CSRF attacks. By default, the origin matching the base URL is trusted, but you can set a list of trusted origins in the trustedOrigins configuration option.
## OAuth State and PKCE
To secure OAuth flows, Better Auth stores the OAuth state and PKCE (Proof Key for Code Exchange) in the database. The state helps prevent CSRF attacks, while PKCE protects against code injection threats. Once the OAuth process completes, these values are removed from the database.
## Cookies
Better Auth assigns secure cookies by default when the base URL uses `https`. These secure cookies are encrypted and only sent over secure connections, adding an extra layer of protection. They are also set with the `sameSite` attribute to `lax` by default to prevent cross-site request forgery attacks. And the `httpOnly` attribute is enabled to prevent client-side JavaScript from accessing the cookie.
For Cross-Subdomain Cookies, you can set the `crossSubDomainCookies` option in the configuration. This option allows cookies to be shared across subdomains, enabling seamless authentication across multiple subdomains.
### Customizing Cookies
You can customize cookie names to minimize the risk of fingerprinting attacks and set specific cookie options as needed for additional control. For more information, refer to the [cookie options](/docs/concepts/cookies).
Plugins can also set custom cookie options to align with specific security needs. If you're using Better Auth in non-browser environments, plugins offer ways to manage cookies securely in those contexts as well.
## Rate Limiting
Better Auth includes built-in rate limiting to safeguard against brute-force attacks. Rate limits are applied across all routes by default, with specific routes subject to stricter limits based on potential risk.
## IP Address Headers
Better Auth uses client IP addresses for rate limiting and security monitoring. By default, it reads the IP address from the standard `X-Forwarded-For` header. However, you can configure a specific trusted header to ensure accurate IP address detection and prevent IP spoofing attacks.
You can configure the IP address header in your Better Auth configuration:
```typescript
{
advanced: {
ipAddress: {
ipAddressHeaders: ['cf-connecting-ip'] // or any other custom header
}
}
}
```
This ensures that Better Auth only accepts IP addresses from your trusted proxy's header, making it more difficult for attackers to bypass rate limiting or other IP-based security measures by spoofing headers.
> **Important**: When setting a custom IP address header, ensure that your proxy or load balancer is properly configured to set this header, and that it cannot be set by end users directly.
## Trusted Origins
Trusted origins prevent CSRF attacks and block open redirects. You can set a list of trusted origins in the `trustedOrigins` configuration option. Requests from origins not on this list are automatically blocked.
### Basic Usage
The most basic usage is to specify exact origins:
```typescript
{
trustedOrigins: [
"https://example.com",
"https://app.example.com",
"http://localhost:3000"
]
}
```
### Wildcard Domains
Better Auth supports wildcard patterns in trusted origins, which allows you to trust multiple subdomains with a single entry:
```typescript
{
trustedOrigins: [
"*.example.com", // Trust all subdomains of example.com (any protocol)
"https://*.example.com", // Trust only HTTPS subdomains of example.com
"http://*.dev.example.com" // Trust all HTTP subdomains of dev.example.com
]
}
```
#### Protocol-specific wildcards
When using a wildcard pattern with a protocol prefix (like `https://`):
* The protocol must match exactly
* The domain can have any subdomain in place of the `*`
* Requests using a different protocol will be rejected, even if the domain matches
#### Protocol-agnostic wildcards
When using a wildcard pattern without a protocol prefix (like `*.example.com`):
* Any protocol (http, https, etc.) will be accepted
* The domain must match the wildcard pattern
### Custom Schemes
Trusted origins also support custom schemes for mobile apps and browser extensions:
```typescript
{
trustedOrigins: [
"myapp://", // Mobile app scheme
"chrome-extension://YOUR_EXTENSION_ID" // Browser extension
]
}
```
## Reporting Vulnerabilities
If you discover a security vulnerability in Better Auth, please report it to us at [security@better-auth.com](mailto:security@better-auth.com). We address all reports promptly, and credits will be given for validated discoveries.
# reference: Telemetry
URL: /docs/reference/telemetry
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/reference/telemetry.mdx
Better Auth now collects anonymous telemetry data about general usage.
***
title: Telemetry
description: Better Auth now collects anonymous telemetry data about general usage.
-----------------------------------------------------------------------------------
Better Auth collects anonymous usage data to help us improve the project. This is optional, transparent, and disabled by default.
## Why is telemetry collected?
Since v1.3.5, Better Auth collects anonymous telemetry data about general usage if enabled.
Telemetry data helps us understand how Better Auth is being used across different environments so we can improve performance, prioritize features, and fix issues more effectively. It guides our decisions on performance optimizations, feature development, and bug fixes. All data is collected completely anonymously and with privacy in mind, and users can opt out at any time. We strive to keep what we collect as transparent as possible.
## What is being collected?
The following data points may be reported. Everything is anonymous and intended for aggregate insights only.
* **Anonymous identifier**: A non-reversible hash derived from your project (`package.json` name and optionally `baseURL`). This lets us de‑duplicate events per project without knowing who you are.
* **Runtime**: `{ name: "node" | "bun" | "deno", version }`.
* **Environment**: one of `development`, `production`, `test`, or `ci`.
* **Framework (if detected)**: `{ name, version }` for frameworks like Next.js, Nuxt, Remix, Astro, SvelteKit, etc.
* **Database (if detected)**: `{ name, version }` for integrations like PostgreSQL, MySQL, SQLite, Prisma, Drizzle, MongoDB, etc.
* **System info**: platform, OS release, architecture, CPU count/model/speed, total memory, and flags like `isDocker`, `isWSL`, `isTTY`.
* **Package manager**: `{ name, version }` derived from the npm user agent.
* **Redacted auth config snapshot**: A minimized, privacy‑preserving view of your `betterAuth` options produced by `getTelemetryAuthConfig`.
We also collect anonymous telemetry from the CLI:
* **CLI generate (`cli_generate`)**: outcome `generated | overwritten | appended | no_changes | aborted` plus redacted config.
* **CLI migrate (`cli_migrate`)**: outcome `migrated | no_changes | aborted | unsupported_adapter` plus adapter id (when relevant) and redacted config.
You can audit telemetry locally by setting the `BETTER_AUTH_TELEMETRY_DEBUG=1` environment variable when running your project or by setting `telemetry: { debug: true }` in your auth config. In this debug mode, telemetry events are logged only to the console.
```ts title="auth.ts"
export const auth = betterAuth({
// [!code highlight]
telemetry: { // [!code highlight]
debug: true // [!code highlight]
} // [!code highlight]
});
```
## How is my data protected?
All collected data is fully anonymous and only useful in aggregate. It cannot be traced back to any individual source and is accessible only to a small group of core Better Auth maintainers to guide roadmap decisions.
* **No PII or secrets**: We do not collect emails, usernames, tokens, secrets, client IDs, client secrets, or database URLs.
* **No full config**: We never send your full `betterAuth` configuration. Instead we send a reduced, redacted snapshot of non‑sensitive toggles and counts.
* **Redaction by design**: See [detect-auth-config.ts](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/telemetry/detectors/detect-auth-config.ts) in the Better Auth source for the exact shape of what is included. It purposely converts sensitive values to booleans, counts, or generic identifiers.
## How can I enable it?
You can enable telemetry collection in your auth config or by setting an environment variable.
* Via your auth config.
```ts title="auth.ts"
export const auth = betterAuth({
// [!code highlight]
telemetry: { // [!code highlight]
enabled: true// [!code highlight]
} // [!code highlight]
});
```
* Via an environment variable.
```txt title=".env"
# Enable telemetry
BETTER_AUTH_TELEMETRY=1
# Disable telemetry
BETTER_AUTH_TELEMETRY=0
```
### When is telemetry sent?
* On `betterAuth` initialization (`type: "init"`).
* On CLI actions: `generate` and `migrate` as described above.
Telemetry is disabled automatically in tests (`NODE_ENV=test`) unless explicitly overridden by internal tooling.
# plugins: Two-Factor Authentication (2FA)
URL: /docs/plugins/2fa
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/2fa.mdx
Enhance your app's security with two-factor authentication.
***
title: Two-Factor Authentication (2FA)
description: Enhance your app's security with two-factor authentication.
------------------------------------------------------------------------
`OTP` `TOTP` `Backup Codes` `Trusted Devices`
Two-Factor Authentication (2FA) adds an extra security step when users log in. Instead of just using a password, they'll need to provide a second form of verification. This makes it much harder for unauthorized people to access accounts, even if they've somehow gotten the password.
This plugin offers two main methods to do a second factor verification:
1. **OTP (One-Time Password)**: A temporary code sent to the user's email or phone.
2. **TOTP (Time-based One-Time Password)**: A code generated by an app on the user's device.
**Additional features include:**
* Generating backup codes for account recovery
* Enabling/disabling 2FA
* Managing trusted devices
## Installation
### Add the plugin to your auth config
Add the two-factor plugin to your auth configuration and specify your app name as the issuer.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
appName: "My App", // provide your app name. It'll be used as an issuer. // [!code highlight]
plugins: [
twoFactor() // [!code highlight]
]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
Add the client plugin and Specify where the user should be redirected if they need to verify 2nd factor
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
twoFactorClient()
]
})
```
## Usage
### Enabling 2FA
To enable two-factor authentication, call `twoFactor.enable` with the user's password and issuer (optional):
### Client Side
```ts
const { data, error } = await authClient.twoFactor.enable({
password: secure-password,
issuer: my-app-name, // required
});
```
### Server Side
```ts
const data = await auth.api.enableTwoFactor({
body: {
password: secure-password,
issuer: my-app-name, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type enableTwoFactor = {
/**
* The user's password
*/
password: string = "secure-password"
/**
* An optional custom issuer for the TOTP URI. Defaults to app-name defined in your auth config.
*/
issuer?: string = "my-app-name"
}
```
When 2FA is enabled:
* An encrypted `secret` and `backupCodes` are generated.
* `enable` returns `totpURI` and `backupCodes`.
Note: `twoFactorEnabled` won’t be set to `true` until the user verifies their TOTP code. Learn more about veryifying TOTP [here](#totp). You can skip verification by setting `skipVerificationOnEnable` to true in your plugin config.
Two Factor can only be enabled for credential accounts at the moment. For social accounts, it's assumed the provider already handles 2FA.
### Sign In with 2FA
When a user with 2FA enabled tries to sign in via email, the response object will contain `twoFactorRedirect` set to `true`. This indicates that the user needs to verify their 2FA code.
You can handle this in the `onSuccess` callback or by providing a `onTwoFactorRedirect` callback in the plugin config.
```ts title="sign-in.tsx"
await authClient.signIn.email({
email: "user@example.com",
password: "password123",
},
{
async onSuccess(context) {
if (context.data.twoFactorRedirect) {
// Handle the 2FA verification in place
}
},
}
)
```
Using the `onTwoFactorRedirect` config:
```ts title="sign-in.ts"
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect(){
// Handle the 2FA verification globally
},
}),
],
});
```
**With `auth.api`**
When you call `auth.api.signInEmail` on the server, and the user has 2FA enabled, it will return an object where `twoFactorRedirect` is set to `true`. This behavior isn’t inferred in TypeScript, which can be misleading. You can check using `in` instead to check if `twoFactorRedirect` is set to `true`.
```ts
const response = await auth.api.signInEmail({
body: {
email: "test@test.com",
password: "test",
},
});
if ("twoFactorRedirect" in response) {
// Handle the 2FA verification in place
}
```
### Disabling 2FA
To disable two-factor authentication, call `twoFactor.disable` with the user's password:
### Client Side
```ts
const { data, error } = await authClient.twoFactor.disable({
password,
});
```
### Server Side
```ts
const data = await auth.api.disableTwoFactor({
body: {
password,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type disableTwoFactor = {
/**
* The user's password
*/
password: string
}
```
### TOTP
TOTP (Time-Based One-Time Password) is an algorithm that generates a unique password for each login attempt using time as a counter. Every fixed interval (Better Auth defaults to 30 seconds), a new password is generated. This addresses several issues with traditional passwords: they can be forgotten, stolen, or guessed. OTPs solve some of these problems, but their delivery via SMS or email can be unreliable (or even risky, considering it opens new attack vectors).
TOTP, however, generates codes offline, making it both secure and convenient. You just need an authenticator app on your phone.
#### Getting TOTP URI
After enabling 2FA, you can get the TOTP URI to display to the user. This URI is generated by the server using the `secret` and `issuer` and can be used to generate a QR code for the user to scan with their authenticator app.
### Client Side
```ts
const { data, error } = await authClient.twoFactor.getTotpUri({
password,
});
```
### Server Side
```ts
const data = await auth.api.getTOTPURI({
body: {
password,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getTOTPURI = {
/**
* The user's password
*/
password: string
}
```
**Example: Using React**
Once you have the TOTP URI, you can use it to generate a QR code for the user to scan with their authenticator app.
```tsx title="user-card.tsx"
import QRCode from "react-qr-code";
export default function UserCard({ password }: { password: string }){
const { data: session } = client.useSession();
const { data: qr } = useQuery({
queryKey: ["two-factor-qr"],
queryFn: async () => {
const res = await authClient.twoFactor.getTotpUri({ password });
return res.data;
},
enabled: !!session?.user.twoFactorEnabled,
});
return (
)
}
```
By default the issuer for TOTP is set to the app name provided in the auth config or if not provided it will be set to `Better Auth`. You can override this by passing `issuer` to the plugin config.
#### Verifying TOTP
After the user has entered their 2FA code, you can verify it using `twoFactor.verifyTotp` method. `Better Auth` follows standard practice by accepting TOTP codes from one period before and one after the current code, ensuring users can authenticate even with minor time delays on their end.
### Client Side
```ts
const { data, error } = await authClient.twoFactor.verifyTotp({
code: 012345,
trustDevice, // required
});
```
### Server Side
```ts
const data = await auth.api.verifyTOTP({
body: {
code: 012345,
trustDevice, // required
}
});
```
### Type Definition
```ts
type verifyTOTP = {
/**
* The otp code to verify.
*/
code: string = "012345"
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
### OTP
OTP (One-Time Password) is similar to TOTP but a random code is generated and sent to the user's email or phone.
Before using OTP to verify the second factor, you need to configure `sendOTP` in your Better Auth instance. This function is responsible for sending the OTP to the user's email, phone, or any other method supported by your application.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }, request) {
// send otp to user
},
},
})
]
})
```
#### Sending OTP
Sending an OTP is done by calling the `twoFactor.sendOtp` function. This function will trigger your sendOTP implementation that you provided in the Better Auth configuration.
### Client Side
```ts
const { data, error } = await authClient.twoFactor.sendOtp({
trustDevice, // required
});
```
### Server Side
```ts
const data = await auth.api.send2FaOTP({
body: {
trustDevice, // required
}
});
```
### Type Definition
```ts
type send2FaOTP = {
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
#### Verifying OTP
After the user has entered their OTP code, you can verify it
### Client Side
```ts
const { data, error } = await authClient.twoFactor.verifyOtp({
code: 012345,
trustDevice, // required
});
```
### Server Side
```ts
const data = await auth.api.verifyOTP({
body: {
code: 012345,
trustDevice, // required
}
});
```
### Type Definition
```ts
type verifyOTP = {
/**
* The otp code to verify.
*/
code: string = "012345"
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
### Backup Codes
Backup codes are generated and stored in the database. This can be used to recover access to the account if the user loses access to their phone or email.
#### Generating Backup Codes
Generate backup codes for account recovery:
### Client Side
```ts
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password,
});
```
### Server Side
```ts
const data = await auth.api.generateBackupCodes({
body: {
password,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type generateBackupCodes = {
/**
* The users password.
*/
password: string
}
```
When you generate backup codes, the old backup codes will be deleted and new ones will be generated.
#### Using Backup Codes
You can now allow users to provider backup code as account recover method.
### Client Side
```ts
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code: 123456,
disableSession, // required
trustDevice, // required
});
```
### Server Side
```ts
const data = await auth.api.verifyBackupCode({
body: {
code: 123456,
disableSession, // required
trustDevice, // required
}
});
```
### Type Definition
```ts
type verifyBackupCode = {
/**
* A backup code to verify.
*/
code: string = "123456"
/**
* If true, the session cookie will not be set.
*/
disableSession?: boolean = false
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
Once a backup code is used, it will be removed from the database and can't be used again.
#### Viewing Backup Codes
To display the backup codes to the user, you can call `viewBackupCodes` on the server. This will return the backup codes in the response. You should only this if the user has a fresh session - a session that was just created.
### Client Side
```ts
const { data, error } = await authClient.twoFactor.viewBackupCodes({
userId: user-id, // required
});
```
### Server Side
```ts
const data = await auth.api.viewBackupCodes({
body: {
userId: user-id, // required
}
});
```
### Type Definition
```ts
type viewBackupCodes = {
/**
* The user ID to view all backup codes.
*/
userId?: string | null = "user-id"
}
```
### Trusted Devices
You can mark a device as trusted by passing `trustDevice` to `verifyTotp` or `verifyOtp`.
```ts
const verify2FA = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
callbackURL: "/dashboard",
trustDevice: true // Mark this device as trusted
})
if (data) {
// 2FA verified and device trusted
}
}
```
When `trustDevice` is set to `true`, the current device will be remembered for 60 days. During this period, the user won't be prompted for 2FA on subsequent sign-ins from this device. The trust period is refreshed each time the user signs in successfully.
### Issuer
By adding an `issuer` you can set your application name for the 2fa application.
For example, if your user uses Google Auth, the default appName will show up as `Better Auth`. However, by using the following code, it will show up as `my-app-name`.
```ts
twoFactor({
issuer: "my-app-name" // [!code highlight]
})
```
***
## Schema
The plugin requires 1 additional fields in the `user` table and 1 additional table to store the two factor authentication data.
Table: `user`
Table: `twoFactor`
## Options
### Server
**twoFactorTable**: The name of the table that stores the two factor authentication data. Default: `twoFactor`.
**skipVerificationOnEnable**: Skip the verification process before enabling two factor for a user.
**Issuer**: The issuer is the name of your application. It's used to generate TOTP codes. It'll be displayed in the authenticator apps.
**TOTP options**
these are options for TOTP.
**OTP options**
these are options for OTP.
**Backup Code Options**
backup codes are generated and stored in the database when the user enabled two factor authentication. This can be used to recover access to the account if the user loses access to their phone or email.
### Client
To use the two factor plugin in the client, you need to add it on your plugins list.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [
twoFactorClient({ // [!code highlight]
onTwoFactorRedirect(){ // [!code highlight]
window.location.href = "/2fa" // Handle the 2FA verification redirect // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
**Options**
`onTwoFactorRedirect`: A callback that will be called when the user needs to verify their 2FA code. This can be used to redirect the user to the 2FA page.
# plugins: Admin
URL: /docs/plugins/admin
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/admin.mdx
Admin plugin for Better Auth
***
title: Admin
description: Admin plugin for Better Auth
-----------------------------------------
The Admin plugin provides a set of administrative functions for user management in your application. It allows administrators to perform various operations such as creating users, managing user roles, banning/unbanning users, impersonating users, and more.
## Installation
### Add the plugin to your auth config
To use the Admin plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { admin } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
admin() // [!code highlight]
]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
Next, include the admin client plugin in your authentication client instance.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { adminClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
adminClient()
]
})
```
## Usage
Before performing any admin operations, the user must be authenticated with an admin account. An admin is any user assigned the `admin` role or any user whose ID is included in the `adminUserIds` option.
### Create User
Allows an admin to create a new user.
### Client Side
```ts
const { data, error } = await authClient.admin.createUser({
email: user@example.com,
password: some-secure-password,
name: James Smith,
role: user, // required
data, // required
});
```
### Server Side
```ts
const newUser = await auth.api.createUser({
body: {
email: user@example.com,
password: some-secure-password,
name: James Smith,
role: user, // required
data, // required
}
});
```
### Type Definition
```ts
type createUser = {
/**
* The email of the user.
*/
email: string = "user@example.com"
/**
* The password of the user.
*/
password: string = "some-secure-password"
/**
* The name of the user.
*/
name: string = "James Smith"
/**
* A string or array of strings representing the roles to apply to the new user.
*/
role?: string | string[] = "user"
/**
* Extra fields for the user. Including custom additional fields.
*/
data?: Record = { customField: "customValue"
}
```
### List Users
Allows an admin to list all users in the database.
### Client Side
```ts
const { data, error } = await authClient.admin.listUsers({
query, // required
searchValue: some name, // required
searchField: name, // required
searchOperator: contains, // required
limit, // required
offset, // required
sortBy: name, // required
sortDirection: desc, // required
filterField: email, // required
filterValue: hello@example.com, // required
filterOperator: eq, // required
});
```
### Server Side
```ts
const data = await auth.api.listUsers({
query: {
query, // required
searchValue: some name, // required
searchField: name, // required
searchOperator: contains, // required
limit, // required
offset, // required
sortBy: name, // required
sortDirection: desc, // required
filterField: email, // required
filterValue: hello@example.com, // required
filterOperator: eq, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listUsers = {
/**
* Query parameters for filtering, searching, and pagination.
*/
query?: {
/**
* The value to search for.
*/
searchValue?: string = "some name"
/**
* The field to search in, defaults to email. Can be `email` or `name`.
*/
searchField?: "email" | "name" = "name"
/**
* The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`.
*/
searchOperator?: "contains" | "starts_with" | "ends_with" = "contains"
/**
* The number of users to return. Defaults to 100.
*/
limit?: string | number = 100
/**
* The offset to start from.
*/
offset?: string | number = 100
/**
* The field to sort by.
*/
sortBy?: string = "name"
/**
* The direction to sort by.
*/
sortDirection?: "asc" | "desc" = "desc"
/**
* The field to filter by.
*/
filterField?: string = "email"
/**
* The value to filter by.
*/
filterValue?: string | number | boolean = "hello@example.com"
/**
* The operator to use for the filter.
*/
filterOperator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" = "eq"
}
```
#### Query Filtering
The `listUsers` function supports various filter operators including `eq`, `contains`, `starts_with`, and `ends_with`.
#### Pagination
The `listUsers` function supports pagination by returning metadata alongside the user list. The response includes the following fields:
```ts
{
users: User[], // Array of returned users
total: number, // Total number of users after filters and search queries
limit: number | undefined, // The limit provided in the query
offset: number | undefined // The offset provided in the query
}
```
##### How to Implement Pagination
To paginate results, use the `total`, `limit`, and `offset` values to calculate:
* **Total pages:** `Math.ceil(total / limit)`
* **Current page:** `(offset / limit) + 1`
* **Next page offset:** `Math.min(offset + limit, (total - 1))` – The value to use as `offset` for the next page, ensuring it does not exceed the total number of pages.
* **Previous page offset:** `Math.max(0, offset - limit)` – The value to use as `offset` for the previous page (ensuring it doesn’t go below zero).
##### Example Usage
Fetching the second page with 10 users per page:
```ts title="admin.ts"
const pageSize = 10;
const currentPage = 2;
const users = await authClient.admin.listUsers({
query: {
limit: pageSize,
offset: (currentPage - 1) * pageSize
}
});
const totalUsers = users.total;
const totalPages = Math.ceil(totalUsers / pageSize)
```
### Set User Role
Changes the role of a user.
### Client Side
```ts
const { data, error } = await authClient.admin.setRole({
userId: user-id, // required
role: admin,
});
```
### Server Side
```ts
const data = await auth.api.setRole({
body: {
userId: user-id, // required
role: admin,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type setRole = {
/**
* The user id which you want to set the role for.
*/
userId?: string = "user-id"
/**
* The role to set, this can be a string or an array of strings.
*/
role: string | string[] = "admin"
}
```
### Set User Password
Changes the password of a user.
### Client Side
```ts
const { data, error } = await authClient.admin.setUserPassword({
newPassword: new-password,
userId: user-id,
});
```
### Server Side
```ts
const data = await auth.api.setUserPassword({
body: {
newPassword: new-password,
userId: user-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type setUserPassword = {
/**
* The new password.
*/
newPassword: string = 'new-password'
/**
* The user id which you want to set the password for.
*/
userId: string = 'user-id'
}
```
### Ban User
Bans a user, preventing them from signing in and revokes all of their existing sessions.
### Client Side
```ts
const { data, error } = await authClient.admin.banUser({
userId: user-id,
banReason: Spamming, // required
banExpiresIn, // required
});
```
### Server Side
```ts
await auth.api.banUser({
body: {
userId: user-id,
banReason: Spamming, // required
banExpiresIn, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type banUser = {
/**
* The user id which you want to ban.
*/
userId: string = "user-id"
/**
* The reason for the ban.
*/
banReason?: string = "Spamming"
/**
* The number of seconds until the ban expires. If not provided, the ban will never expire.
*/
banExpiresIn?: number = 60 * 60 * 24 * 7
}
```
### Unban User
Removes the ban from a user, allowing them to sign in again.
### Client Side
```ts
const { data, error } = await authClient.admin.unbanUser({
userId: user-id,
});
```
### Server Side
```ts
await auth.api.unbanUser({
body: {
userId: user-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type unbanUser = {
/**
* The user id which you want to unban.
*/
userId: string = "user-id"
}
```
### List User Sessions
Lists all sessions for a user.
### Client Side
```ts
const { data, error } = await authClient.admin.listUserSessions({
userId: user-id,
});
```
### Server Side
```ts
const data = await auth.api.listUserSessions({
body: {
userId: user-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listUserSessions = {
/**
* The user id.
*/
userId: string = "user-id"
}
```
### Revoke User Session
Revokes a specific session for a user.
### Client Side
```ts
const { data, error } = await authClient.admin.revokeUserSession({
sessionToken: session_token_here,
});
```
### Server Side
```ts
const data = await auth.api.revokeUserSession({
body: {
sessionToken: session_token_here,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type revokeUserSession = {
/**
* The session token which you want to revoke.
*/
sessionToken: string = "session_token_here"
}
```
### Revoke All Sessions for a User
Revokes all sessions for a user.
### Client Side
```ts
const { data, error } = await authClient.admin.revokeUserSessions({
userId: user-id,
});
```
### Server Side
```ts
const data = await auth.api.revokeUserSessions({
body: {
userId: user-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type revokeUserSessions = {
/**
* The user id which you want to revoke all sessions for.
*/
userId: string = "user-id"
}
```
### Impersonate User
This feature allows an admin to create a session that mimics the specified user. The session will remain active until either the browser session ends or it reaches 1 hour. You can change this duration by setting the `impersonationSessionDuration` option.
### Client Side
```ts
const { data, error } = await authClient.admin.impersonateUser({
userId: user-id,
});
```
### Server Side
```ts
const data = await auth.api.impersonateUser({
body: {
userId: user-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type impersonateUser = {
/**
* The user id which you want to impersonate.
*/
userId: string = "user-id"
}
```
### Stop Impersonating User
To stop impersonating a user and continue with the admin account, you can use `stopImpersonating`
### Client Side
```ts
const { data, error } = await authClient.admin.stopImpersonating({});
```
### Server Side
```ts
await auth.api.stopImpersonating({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type stopImpersonating = {
}
```
### Remove User
Hard deletes a user from the database.
### Client Side
```ts
const { data, error } = await authClient.admin.removeUser({
userId: user-id,
});
```
### Server Side
```ts
const deletedUser = await auth.api.removeUser({
body: {
userId: user-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type removeUser = {
/**
* The user id which you want to remove.
*/
userId: string = "user-id"
}
```
## Access Control
The admin plugin offers a highly flexible access control system, allowing you to manage user permissions based on their role. You can define custom permission sets to fit your needs.
### Roles
By default, there are two roles:
`admin`: Users with the admin role have full control over other users.
`user`: Users with the user role have no control over other users.
A user can have multiple roles. Multiple roles are stored as string separated by comma (",").
### Permissions
By default, there are two resources with up to six permissions.
**user**:
`create` `list` `set-role` `ban` `impersonate` `delete` `set-password`
**session**:
`list` `revoke` `delete`
Users with the admin role have full control over all the resources and actions. Users with the user role have no control over any of those actions.
### Custom Permissions
The plugin provides an easy way to define your own set of permissions for each role.
#### Create Access Control
You first need to create an access controller by calling the `createAccessControl` function and passing the statement object. The statement object should have the resource name as the key and the array of actions as the value.
```ts title="permissions.ts"
import { createAccessControl } from "better-auth/plugins/access";
/**
* make sure to use `as const` so typescript can infer the type correctly
*/
const statement = { // [!code highlight]
project: ["create", "share", "update", "delete"], // [!code highlight]
} as const; // [!code highlight]
const ac = createAccessControl(statement); // [!code highlight]
```
#### Create Roles
Once you have created the access controller you can create roles with the permissions you have defined.
```ts title="permissions.ts"
import { createAccessControl } from "better-auth/plugins/access";
export const statement = {
project: ["create", "share", "update", "delete"], // <-- Permissions available for created roles
} as const;
const ac = createAccessControl(statement);
export const user = ac.newRole({ // [!code highlight]
project: ["create"], // [!code highlight]
}); // [!code highlight]
export const admin = ac.newRole({ // [!code highlight]
project: ["create", "update"], // [!code highlight]
}); // [!code highlight]
export const myCustomRole = ac.newRole({ // [!code highlight]
project: ["create", "update", "delete"], // [!code highlight]
user: ["ban"], // [!code highlight]
}); // [!code highlight]
```
When you create custom roles for existing roles, the predefined permissions for those roles will be overridden. To add the existing permissions to the custom role, you need to import `defaultStatements` and merge it with your new statement, plus merge the roles' permissions set with the default roles.
```ts title="permissions.ts"
import { createAccessControl } from "better-auth/plugins/access";
import { defaultStatements, adminAc } from "better-auth/plugins/admin/access";
const statement = {
...defaultStatements, // [!code highlight]
project: ["create", "share", "update", "delete"],
} as const;
const ac = createAccessControl(statement);
const admin = ac.newRole({
project: ["create", "update"],
...adminAc.statements, // [!code highlight]
});
```
#### Pass Roles to the Plugin
Once you have created the roles you can pass them to the admin plugin both on the client and the server.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { admin as adminPlugin } from "better-auth/plugins"
import { ac, admin, user } from "@/auth/permissions"
export const auth = betterAuth({
plugins: [
adminPlugin({
ac,
roles: {
admin,
user,
myCustomRole
}
}),
],
});
```
You also need to pass the access controller and the roles to the client plugin.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { adminClient } from "better-auth/client/plugins"
import { ac, admin, user, myCustomRole } from "@/auth/permissions"
export const client = createAuthClient({
plugins: [
adminClient({
ac,
roles: {
admin,
user,
myCustomRole
}
})
]
})
```
### Access Control Usage
**Has Permission**:
To check a user's permissions, you can use the `hasPermission` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.admin.hasPermission({
userId: user-id, // required
role: admin, // required
permission, // required
});
```
### Server Side
```ts
const data = await auth.api.userHasPermission({
body: {
userId: user-id, // required
role: admin, // required
permission, // required
}
});
```
### Type Definition
```ts
type userHasPermission = {
/**
* The user id which you want to check the permissions for.
*/
userId?: string = "user-id"
/**
* Check role permissions.
* @serverOnly
*/
role?: string = "admin"
/**
* Optionally check if a single permission is granted. Must use this, or permissions.
*/
permission?: Record = { "project": ["create", "update"]
}
```
Example usage:
```ts title="auth-client.ts"
const canCreateProject = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
},
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
sale: ["create"]
},
});
```
If you want to check a user's permissions server-side, you can use the `userHasPermission` action provided by the `api` to check the user's permissions.
```ts title="api.ts"
import { auth } from "@/auth";
await auth.api.userHasPermission({
body: {
userId: 'id', //the user id
permissions: {
project: ["create"], // This must match the structure in your access control
},
},
});
// You can also just pass the role directly
await auth.api.userHasPermission({
body: {
role: "admin",
permissions: {
project: ["create"], // This must match the structure in your access control
},
},
});
// You can also check multiple resource permissions at the same time
await auth.api.userHasPermission({
body: {
role: "admin",
permissions: {
project: ["create"], // This must match the structure in your access control
sale: ["create"]
},
},
});
```
**Check Role Permission**:
Use the `checkRolePermission` function on the client side to verify whether a given **role** has a specific **permission**. This is helpful after defining roles and their permissions, as it allows you to perform permission checks without needing to contact the server.
Note that this function does **not** check the permissions of the currently logged-in user directly. Instead, it checks what permissions are assigned to a specified role. The function is synchronous, so you don't need to use `await` when calling it.
```ts title="auth-client.ts"
const canCreateProject = authClient.admin.checkRolePermission({
permissions: {
user: ["delete"],
},
role: "admin",
});
// You can also check multiple resource permissions at the same time
const canDeleteUserAndRevokeSession = authClient.admin.checkRolePermission({
permissions: {
user: ["delete"],
session: ["revoke"]
},
role: "admin",
});
```
## Schema
This plugin adds the following fields to the `user` table:
And adds one field in the `session` table:
## Options
### Default Role
The default role for a user. Defaults to `user`.
```ts title="auth.ts"
admin({
defaultRole: "regular",
});
```
### Admin Roles
The roles that are considered admin roles. Defaults to `["admin"]`.
```ts title="auth.ts"
admin({
adminRoles: ["admin", "superadmin"],
});
```
Any role that isn't in the `adminRoles` list, even if they have the permission,
will not be considered an admin.
### Admin userIds
You can pass an array of userIds that should be considered as admin. Default to `[]`
```ts title="auth.ts"
admin({
adminUserIds: ["user_id_1", "user_id_2"]
})
```
If a user is in the `adminUserIds` list, they will be able to perform any admin operation.
### impersonationSessionDuration
The duration of the impersonation session in seconds. Defaults to 1 hour.
```ts title="auth.ts"
admin({
impersonationSessionDuration: 60 * 60 * 24, // 1 day
});
```
### Default Ban Reason
The default ban reason for a user created by the admin. Defaults to `No reason`.
```ts title="auth.ts"
admin({
defaultBanReason: "Spamming",
});
```
### Default Ban Expires In
The default ban expires in for a user created by the admin in seconds. Defaults to `undefined` (meaning the ban never expires).
```ts title="auth.ts"
admin({
defaultBanExpiresIn: 60 * 60 * 24, // 1 day
});
```
### bannedUserMessage
The message to show when a banned user tries to sign in. Defaults to "You have been banned from this application. Please contact support if you believe this is an error."
```ts title="auth.ts"
admin({
bannedUserMessage: "Custom banned user message",
});
```
# plugins: Anonymous
URL: /docs/plugins/anonymous
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/anonymous.mdx
Anonymous plugin for Better Auth.
***
title: Anonymous\
description: Anonymous plugin for Better Auth.
----------------------------------------------
The Anonymous plugin allows users to have an authenticated experience without requiring them to provide an email address, password, OAuth provider, or any other Personally Identifiable Information (PII). Users can later link an authentication method to their account when ready.
## Installation
### Add the plugin to your auth config
To enable anonymous authentication, add the anonymous plugin to your authentication configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { anonymous } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
anonymous() // [!code highlight]
]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
Next, include the anonymous client plugin in your authentication client instance.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { anonymousClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
anonymousClient()
]
})
```
## Usage
### Sign In
To sign in a user anonymously, use the `signIn.anonymous()` method.
```ts title="example.ts"
const user = await authClient.signIn.anonymous()
```
### Link Account
If a user is already signed in anonymously and tries to `signIn` or `signUp` with another method, their anonymous activities can be linked to the new account.
To do that you first need to provide `onLinkAccount` callback to the plugin.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
anonymous({
onLinkAccount: async ({ anonymousUser, newUser }) => {
// perform actions like moving the cart items from anonymous user to the new user
}
})
]
```
Then when you call `signIn` or `signUp` with another method, the `onLinkAccount` callback will be called. And the `anonymousUser` will be deleted by default.
```ts title="example.ts"
const user = await authClient.signIn.email({
email,
})
```
## Options
* `emailDomainName`: The domain name to use when generating an email address for anonymous users. Defaults to the domain name of the current site.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
anonymous({
emailDomainName: "example.com"
})
]
})
```
* `onLinkAccount`: A callback function that is called when an anonymous user links their account to a new authentication method. The callback receives an object with the `anonymousUser` and the `newUser`.
* `disableDeleteAnonymousUser`: By default, the anonymous user is deleted when the account is linked to a new authentication method. Set this option to `true` to disable this behavior.
* `generateName`: A callback function that is called to generate a name for the anonymous user. Useful if you want to have random names for anonymous users, or if `name` is unique in your database.
## Schema
The anonymous plugin requires an additional field in the user table:
# plugins: API Key
URL: /docs/plugins/api-key
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/api-key.mdx
API Key plugin for Better Auth.
***
title: API Key
description: API Key plugin for Better Auth.
--------------------------------------------
The API Key plugin allows you to create and manage API keys for your application. It provides a way to authenticate and authorize API requests by verifying API keys.
## Features
* Create, manage, and verify API keys
* [Built-in rate limiting](/docs/plugins/api-key#rate-limiting)
* [Custom expiration times, remaining count, and refill systems](/docs/plugins/api-key#remaining-refill-and-expiration)
* [metadata for API keys](/docs/plugins/api-key#metadata)
* Custom prefix
* [Sessions from API keys](/docs/plugins/api-key#sessions-from-api-keys)
## Installation
### Add Plugin to the server
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { apiKey } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
apiKey() // [!code highlight]
] // [!code highlight]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { apiKeyClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
apiKeyClient() // [!code highlight]
] // [!code highlight]
})
```
## Usage
You can view the list of API Key plugin options [here](/docs/plugins/api-key#api-key-plugin-options).
### Create an API key
### Client Side
```ts
const { data, error } = await authClient.apiKey.create({
name: project-api-key, // required
expiresIn, // required
userId: user-id, // required
prefix: project-api-key, // required
remaining, // required
metadata, // required
});
```
### Server Side
```ts
const data = await auth.api.createApiKey({
body: {
name: project-api-key, // required
expiresIn, // required
userId: user-id, // required
prefix: project-api-key, // required
remaining, // required
metadata, // required
}
});
```
### Type Definition
```ts
type createApiKey = {
/**
* Name of the Api Key.
*/
name?: string = 'project-api-key'
/**
* Expiration time of the Api Key in seconds.
*/
expiresIn?: number = 60 * 60 * 24 * 7
/**
* User Id of the user that the Api Key belongs to. server-only.
* @serverOnly
*/
userId?: string = "user-id"
/**
* Prefix of the Api Key.
*/
prefix?: string = 'project-api-key'
/**
* Remaining number of requests. server-only.
* @serverOnly
*/
remaining?: number = 100
/**
* Metadata of the Api Key.
*/
metadata?: any | null = { someKey: 'someValue'
}
```
API keys are assigned to a user.
#### Result
It'll return the `ApiKey` object which includes the `key` value for you to use.
Otherwise if it throws, it will throw an `APIError`.
***
### Verify an API key
### Client Side
```ts
const { data, error } = await authClient.apiKey.verify({
key: your_api_key_here,
permissions, // required
});
```
### Server Side
```ts
const data = await auth.api.verifyApiKey({
body: {
key: your_api_key_here,
permissions, // required
}
});
```
### Type Definition
```ts
type verifyApiKey = {
/**
* The key to verify.
*/
key: string = "your_api_key_here"
/**
* The permissions to verify. Optional.
*/
permissions?: Record
}
```
#### Result
```ts
type Result = {
valid: boolean;
error: { message: string; code: string } | null;
key: Omit | null;
};
```
***
### Get an API key
### Client Side
```ts
const { data, error } = await authClient.apiKey.get({
id: some-api-key-id,
});
```
### Server Side
```ts
const data = await auth.api.getApiKey({
query: {
id: some-api-key-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getApiKey = {
/**
* The id of the Api Key.
*/
id: string = "some-api-key-id"
}
```
#### Result
You'll receive everything about the API key details, except for the `key` value itself.
If it fails, it will throw an `APIError`.
```ts
type Result = Omit;
```
***
### Update an API key
### Client Side
```ts
const { data, error } = await authClient.apiKey.update({
keyId: some-api-key-id,
userId: some-user-id, // required
name: some-api-key-name, // required
enabled, // required
remaining, // required
refillAmount, // required
refillInterval, // required
metadata, // required
});
```
### Server Side
```ts
const data = await auth.api.updateApiKey({
body: {
keyId: some-api-key-id,
userId: some-user-id, // required
name: some-api-key-name, // required
enabled, // required
remaining, // required
refillAmount, // required
refillInterval, // required
metadata, // required
}
});
```
### Type Definition
```ts
type updateApiKey = {
/**
* The id of the Api Key to update.
*/
keyId: string = "some-api-key-id"
/**
* The id of the user which the api key belongs to. server-only.
* @serverOnly
*/
userId?: string = "some-user-id"
/**
* The name of the key.
*/
name?: string = "some-api-key-name"
/**
* Whether the Api Key is enabled or not. server-only.
* @serverOnly
*/
enabled?: boolean = true
/**
* The number of remaining requests. server-only.
* @serverOnly
*/
remaining?: number = 100
/**
* The refill amount. server-only.
* @serverOnly
*/
refillAmount?: number = 100
/**
* The refill interval in milliseconds. server-only.
* @serverOnly
*/
refillInterval?: number = 1000
/**
* The metadata of the Api Key. server-only.
* @serverOnly
*/
metadata?: any | null = { "key": "value"
}
```
#### Result
If fails, throws `APIError`.
Otherwise, you'll receive the API Key details, except for the `key` value itself.
***
### Delete an API Key
### Client Side
```ts
const { data, error } = await authClient.apiKey.delete({
keyId: some-api-key-id,
});
```
### Server Side
```ts
const data = await auth.api.deleteApiKey({
body: {
keyId: some-api-key-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type deleteApiKey = {
/**
* The id of the Api Key to delete.
*/
keyId: string = "some-api-key-id"
}
```
#### Result
If fails, throws `APIError`.
Otherwise, you'll receive:
```ts
type Result = {
success: boolean;
};
```
***
### List API keys
### Client Side
```ts
const { data, error } = await authClient.apiKey.list({});
```
### Server Side
```ts
const data = await auth.api.listApiKeys({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listApiKeys = {
}
```
#### Result
If fails, throws `APIError`.
Otherwise, you'll receive:
```ts
type Result = ApiKey[];
```
***
### Delete all expired API keys
This function will delete all API keys that have an expired expiration date.
### Client Side
```ts
const { data, error } = await authClient.apiKey.deleteAllExpiredApiKeys({});
```
### Server Side
```ts
const data = await auth.api.deleteAllExpiredApiKeys({});
```
### Type Definition
```ts
type deleteAllExpiredApiKeys = {
}
```
We automatically delete expired API keys every time any apiKey plugin
endpoints were called, however they are rate-limited to a 10 second cool down
each call to prevent multiple calls to the database.
***
## Sessions from API keys
Any time an endpoint in Better Auth is called that has a valid API key in the headers, we will automatically create a mock session to represent the user.
```ts
const session = await auth.api.getSession({
headers: new Headers({
'x-api-key': apiKey,
}),
});
```
The default header key is `x-api-key`, but this can be changed by setting the `apiKeyHeaders` option in the plugin options.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
apiKeyHeaders: ["x-api-key", "xyz-api-key"], // or you can pass just a string, eg: "x-api-key"
}),
],
});
```
Or optionally, you can pass an `apiKeyGetter` function to the plugin options, which will be called with the `GenericEndpointContext`, and from there, you should return the API key, or `null` if the request is invalid.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
apiKeyGetter: (ctx) => {
const has = ctx.request.headers.has("x-api-key");
if (!has) return null;
return ctx.request.headers.get("x-api-key");
},
}),
],
});
```
## Rate Limiting
Every API key can have its own rate limit settings, however, the built-in rate-limiting only applies to the verification process for a given API key.
For every other endpoint/method, you should utilize Better Auth's [built-in rate-limiting](/docs/concepts/rate-limit).
You can refer to the rate-limit default configurations below in the API Key plugin options.
An example default value:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
rateLimit: {
enabled: true,
timeWindow: 1000 * 60 * 60 * 24, // 1 day
maxRequests: 10, // 10 requests per day
},
}),
],
});
```
For each API key, you can customize the rate-limit options on create.
You can only customize the rate-limit options on the server auth instance.
```ts
const apiKey = await auth.api.createApiKey({
body: {
rateLimitEnabled: true,
rateLimitTimeWindow: 1000 * 60 * 60 * 24, // 1 day
rateLimitMax: 10, // 10 requests per day
},
headers: user_headers,
});
```
### How does it work?
For each request, a counter (internally called `requestCount`) is incremented.\
If the `rateLimitMax` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
## Remaining, refill, and expiration
The remaining count is the number of requests left before the API key is disabled.\
The refill interval is the interval in milliseconds where the `remaining` count is refilled by day.\
The expiration time is the expiration date of the API key.
### How does it work?
#### Remaining:
Whenever an API key is used, the `remaining` count is updated.\
If the `remaining` count is `null`, then there is no cap to key usage.\
Otherwise, the `remaining` count is decremented by 1.\
If the `remaining` count is 0, then the API key is disabled & removed.
#### refillInterval & refillAmount:
Whenever an API key is created, the `refillInterval` and `refillAmount` are set to `null`.\
This means that the API key will not be refilled automatically.\
However, if `refillInterval` & `refillAmount` are set, then the API key will be refilled accordingly.
#### Expiration:
Whenever an API key is created, the `expiresAt` is set to `null`.\
This means that the API key will never expire.\
However, if the `expiresIn` is set, then the API key will expire after the `expiresIn` time.
## Custom Key generation & verification
You can customize the key generation and verification process straight from the plugin options.
Here's an example:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
customKeyGenerator: (options: {
length: number;
prefix: string | undefined;
}) => {
const apiKey = mySuperSecretApiKeyGenerator(
options.length,
options.prefix
);
return apiKey;
},
customAPIKeyValidator: async ({ ctx, key }) => {
const res = await keyService.verify(key)
return res.valid
},
}),
],
});
```
If you're **not** using the `length` property provided by `customKeyGenerator`, you **must** set the `defaultKeyLength` property to how long generated keys will be.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
customKeyGenerator: () => {
return crypto.randomUUID();
},
defaultKeyLength: 36, // Or whatever the length is
}),
],
});
```
If an API key is validated from your `customAPIKeyValidator`, we still must match that against the database's key.
However, by providing this custom function, you can improve the performance of the API key verification process,
as all failed keys can be invalidated without having to query your database.
## Metadata
We allow you to store metadata alongside your API keys. This is useful for storing information about the key, such as a subscription plan for example.
To store metadata, make sure you haven't disabled the metadata feature in the plugin options.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
enableMetadata: true,
}),
],
});
```
Then, you can store metadata in the `metadata` field of the API key object.
```ts
const apiKey = await auth.api.createApiKey({
body: {
metadata: {
plan: "premium",
},
},
});
```
You can then retrieve the metadata from the API key object.
```ts
const apiKey = await auth.api.getApiKey({
body: {
keyId: "your_api_key_id_here",
},
});
console.log(apiKey.metadata.plan); // "premium"
```
## API Key plugin options
`apiKeyHeaders` `string | string[];`
The header name to check for API key. Default is `x-api-key`.
`customAPIKeyGetter` `(ctx: GenericEndpointContext) => string | null`
A custom function to get the API key from the context.
`customAPIKeyValidator` `(options: { ctx: GenericEndpointContext; key: string; }) => boolean | Promise`
A custom function to validate the API key.
`customKeyGenerator` `(options: { length: number; prefix: string | undefined; }) => string | Promise`
A custom function to generate the API key.
`startingCharactersConfig` `{ shouldStore?: boolean; charactersLength?: number; }`
Customize the starting characters configuration.
`shouldStore` `boolean`
Wether to store the starting characters in the database.
If false, we will set `start` to `null`.
Default is `true`.
`charactersLength` `number`
The length of the starting characters to store in the database.
This includes the prefix length.
Default is `6`.
`defaultKeyLength` `number`
The length of the API key. Longer is better. Default is 64. (Doesn't include the prefix length)
`defaultPrefix` `string`
The prefix of the API key.
Note: We recommend you append an underscore to the prefix to make the prefix more identifiable. (eg `hello_`)
`maximumPrefixLength` `number`
The maximum length of the prefix.
`minimumPrefixLength` `number`
The minimum length of the prefix.
`requireName` `boolean`
Whether to require a name for the API key. Default is `false`.
`maximumNameLength` `number`
The maximum length of the name.
`minimumNameLength` `number`
The minimum length of the name.
`enableMetadata` `boolean`
Whether to enable metadata for an API key.
`keyExpiration` `{ defaultExpiresIn?: number | null; disableCustomExpiresTime?: boolean; minExpiresIn?: number; maxExpiresIn?: number; }`
Customize the key expiration.
`defaultExpiresIn` `number | null`
The default expires time in milliseconds.
If `null`, then there will be no expiration time.
Default is `null`.
`disableCustomExpiresTime` `boolean`
Wether to disable the expires time passed from the client.
If `true`, the expires time will be based on the default values.
Default is `false`.
`minExpiresIn` `number`
The minimum expiresIn value allowed to be set from the client. in days.
Default is `1`.
`maxExpiresIn` `number`
The maximum expiresIn value allowed to be set from the client. in days.
Default is `365`.
`rateLimit` `{ enabled?: boolean; timeWindow?: number; maxRequests?: number; }`
Customize the rate-limiting.
`enabled` `boolean`
Whether to enable rate limiting. (Default true)
`timeWindow` `number`
The duration in milliseconds where each request is counted.
Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
`maxRequests` `number`
Maximum amount of requests allowed within a window.
Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
`schema` `InferOptionSchema>`
Custom schema for the API key plugin.
`disableSessionForAPIKeys` `boolean`
An API Key can represent a valid session, so we automatically mock a session for the user if we find a valid API key in the request headers.
`permissions` `{ defaultPermissions?: Statements | ((userId: string, ctx: GenericEndpointContext) => Statements | Promise) }`
Permissions for the API key.
Read more about permissions [here](/docs/plugins/api-key#permissions).
`defaultPermissions` `Statements | ((userId: string, ctx: GenericEndpointContext) => Statements | Promise)`
The default permissions for the API key.
`disableKeyHashing` `boolean`
Disable hashing of the API key.
⚠️ Security Warning: It's strongly recommended to not disable hashing.
Storing API keys in plaintext makes them vulnerable to database breaches, potentially exposing all your users' API keys.
***
## Permissions
API keys can have permissions associated with them, allowing you to control access at a granular level. Permissions are structured as a record of resource types to arrays of allowed actions.
### Setting Default Permissions
You can configure default permissions that will be applied to all newly created API keys:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
permissions: {
defaultPermissions: {
files: ["read"],
users: ["read"],
},
},
}),
],
});
```
You can also provide a function that returns permissions dynamically:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
permissions: {
defaultPermissions: async (userId, ctx) => {
// Fetch user role or other data to determine permissions
return {
files: ["read"],
users: ["read"],
};
},
},
}),
],
});
```
### Creating API Keys with Permissions
When creating an API key, you can specify custom permissions:
```ts
const apiKey = await auth.api.createApiKey({
body: {
name: "My API Key",
permissions: {
files: ["read", "write"],
users: ["read"],
},
userId: "userId",
},
});
```
### Verifying API Keys with Required Permissions
When verifying an API key, you can check if it has the required permissions:
```ts
const result = await auth.api.verifyApiKey({
body: {
key: "your_api_key_here",
permissions: {
files: ["read"],
},
},
});
if (result.valid) {
// API key is valid and has the required permissions
} else {
// API key is invalid or doesn't have the required permissions
}
```
### Updating API Key Permissions
You can update the permissions of an existing API key:
```ts
const apiKey = await auth.api.updateApiKey({
body: {
keyId: existingApiKeyId,
permissions: {
files: ["read", "write", "delete"],
users: ["read", "write"],
},
},
headers: user_headers,
});
```
### Permissions Structure
Permissions follow a resource-based structure:
```ts
type Permissions = {
[resourceType: string]: string[];
};
// Example:
const permissions = {
files: ["read", "write", "delete"],
users: ["read"],
projects: ["read", "write"],
};
```
When verifying an API key, all required permissions must be present in the API key's permissions for validation to succeed.
## Schema
Table: `apiKey`
# plugins: Autumn Billing
URL: /docs/plugins/autumn
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/autumn.mdx
Better Auth Plugin for Autumn Billing
***
title: Autumn Billing
description: Better Auth Plugin for Autumn Billing
--------------------------------------------------
import { HomeIcon } from "lucide-react";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
[Autumn](https://useautumn.com) is open source infrastructure to run SaaS pricing plans. It sits between your app and Stripe, and acts as the database for your customers' subscription status, usage metering and feature permissions.
We're online to help you with any questions you have.
## Features
* One function for all checkout, subscription and payment flows
* No webhooks required: query Autumn for the data you need
* Manages your application's free and paid plans
* Usage tracking for usage billing and periodic limits
* Custom plans and pricing changes through Autumn's dashboard
### Setup Autumn Account
First, create your pricing plans in Autumn's [dashboard](https://app.useautumn.com), where you define what each plan and product gets access to and how it should be billed. In this example, we're handling the free and pro plans for an AI chatbot, which comes with a number of `messages` per month.
### Install Autumn SDK
npm
pnpm
yarn
bun
```bash
npm install autumn-js
```
```bash
pnpm add autumn-js
```
```bash
yarn add autumn-js
```
```bash
bun add autumn-js
```
If you're using a separate client and server setup, make sure to install the plugin in both parts of your project.
### Add `AUTUMN_SECRET_KEY` to your environment variables
You can find it in Autumn's dashboard under "[Developer](https://app.useautumn.com/sandbox/onboarding)".
```bash title=".env"
AUTUMN_SECRET_KEY=am_sk_xxxxxxxxxx
```
### Add the Autumn plugin to your `auth` config
```ts title="auth.ts"
import { autumn } from "autumn-js/better-auth";
export const auth = betterAuth({
// ...
plugins: [autumn()],
});
```
Autumn will auto-create your customers when they sign up, and assign them any
default plans you created (eg your Free plan)
### Add ``
Client side, wrap your application with the AutumnProvider component, and pass in the `baseUrl` that you define within better-auth's `authClient`.
```tsx title="app/layout.tsx"
import { AutumnProvider } from "autumn-js/react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{/* or meta.env.BETTER_AUTH_URL for vite */}
{children}
);
}
```
## Usage
### Handle payments
Call `attach` to redirect the customer to a Stripe checkout page when they want to purchase the Pro plan.
If their payment method is already on file, `AttachDialog` will open instead to let the customer confirm their new subscription or purchase, and handle the payment.
{" "}
Make sure you've pasted in your [Stripe test secret
key](https://dashboard.stripe.com/test/apikeys) in the [Autumn
dashboard](https://app.useautumn.com/integrations/stripe).
```tsx
import { useCustomer, AttachDialog } from "autumn-js/react";
export default function PurchaseButton() {
const { attach } = useCustomer();
return (
);
}
```
The AttachDialog component can be used directly from the `autumn-js/react`
library (as shown in the example above), or downloaded as a [shadcn/ui component](https://docs.useautumn.com/quickstart/shadcn) to customize.
### Integrate Pricing Logic
Integrate your client and server pricing tiers logic with the following functions:
* `check` to see if the customer is `allowed` to send a message.
* `track` a usage event in Autumn (typically done server-side)
* `customer` to display any relevant billing data in your UI (subscriptions, feature balances)
Server-side, you can access Autumn's functions through the `auth` object.
```jsx
import { useCustomer } from "autumn-js/react";
export default function SendChatMessage() {
const { customer, allowed, refetch } = useCustomer();
return (
<>
>
);
}
```
```typescript Server
import { auth } from "@/lib/auth";
// check on the backend if the customer can send a message
const { allowed } = await auth.api.check({
headers: await headers(), // pass the request headers
body: {
feature_id: "messages",
},
});
// server-side function to send the message
// then track the usage
await auth.api.track({
headers: await headers(),
body: {
feature_id: "messages",
value: 2,
},
});
```
### Additional Functions
#### openBillingPortal()
Opens a billing portal where the customer can update their payment method or cancel their plan.
```tsx
import { useCustomer } from "autumn-js/react";
export default function BillingSettings() {
const { openBillingPortal } = useCustomer();
return (
);
}
```
#### cancel()
Cancel a product or subscription.
```tsx
import { useCustomer } from "autumn-js/react";
export default function CancelSubscription() {
const { cancel } = useCustomer();
return (
);
}
```
#### Get invoice history
Pass in an `expand` param into `useCustomer` to get additional information. You can expand `invoices`, `trials_used`, `payment_method`, or `rewards`.
```tsx
import { useCustomer } from "autumn-js/react";
export default function CustomerProfile() {
const { customer } = useCustomer({ expand: ["invoices"] });
return (
);
}
```
# plugins: Bearer Token Authentication
URL: /docs/plugins/bearer
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/bearer.mdx
Authenticate API requests using Bearer tokens instead of browser cookies
***
title: Bearer Token Authentication
description: Authenticate API requests using Bearer tokens instead of browser cookies
-------------------------------------------------------------------------------------
The Bearer plugin enables authentication using Bearer tokens as an alternative to browser cookies. It intercepts requests, adding the Bearer token to the Authorization header before forwarding them to your API.
Use this cautiously; it is intended only for APIs that don't support cookies or require Bearer tokens for authentication. Improper implementation could easily lead to security vulnerabilities.
## Installing the Bearer Plugin
Add the Bearer plugin to your authentication setup:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { bearer } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [bearer()]
});
```
## How to Use Bearer Tokens
### 1. Obtain the Bearer Token
After a successful sign-in, you'll receive a session token in the response headers. Store this token securely (e.g., in `localStorage`):
```ts title="auth-client.ts"
const { data } = await authClient.signIn.email({
email: "user@example.com",
password: "securepassword"
}, {
onSuccess: (ctx)=>{
const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers
// Store the token securely (e.g., in localStorage)
localStorage.setItem("bearer_token", authToken);
}
});
```
You can also set this up globally in your auth client:
```ts title="auth-client.ts"
export const authClient = createAuthClient({
fetchOptions: {
onSuccess: (ctx) => {
const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers
// Store the token securely (e.g., in localStorage)
if(authToken){
localStorage.setItem("bearer_token", authToken);
}
}
}
});
```
You may want to clear the token based on the response status code or other conditions:
### 2. Configure the Auth Client
Set up your auth client to include the Bearer token in all requests:
```ts title="auth-client.ts"
export const authClient = createAuthClient({
fetchOptions: {
auth: {
type:"Bearer",
token: () => localStorage.getItem("bearer_token") || "" // get the token from localStorage
}
}
});
```
### 3. Make Authenticated Requests
Now you can make authenticated API calls:
```ts title="auth-client.ts"
// This request is automatically authenticated
const { data } = await authClient.listSessions();
```
### 4. Per-Request Token (Optional)
You can also provide the token for individual requests:
```ts title="auth-client.ts"
const { data } = await authClient.listSessions({
fetchOptions: {
headers: {
Authorization: `Bearer ${token}`
}
}
});
```
### 5. Using Bearer Tokens Outside the Auth Client
The Bearer token can be used to authenticate any request to your API, even when not using the auth client:
```ts title="api-call.ts"
const token = localStorage.getItem("bearer_token");
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${token}`
}
});
const data = await response.json();
```
And in the server, you can use the `auth.api.getSession` function to authenticate requests:
```ts title="server.ts"
import { auth } from "@/auth";
export async function handler(req, res) {
const session = await auth.api.getSession({
headers: req.headers
});
if (!session) {
return res.status(401).json({ error: "Unauthorized" });
}
// Process authenticated request
// ...
}
```
## Options
**requireSignature** (boolean): Require the token to be signed. Default: `false`.
# plugins: Captcha
URL: /docs/plugins/captcha
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/captcha.mdx
Captcha plugin
***
title: Captcha
description: Captcha plugin
---------------------------
The **Captcha Plugin** integrates bot protection into your Better Auth system by adding captcha verification for key endpoints. This plugin ensures that only human users can perform actions like signing up, signing in, or resetting passwords. The following providers are currently supported:
* [Google reCAPTCHA](https://developers.google.com/recaptcha)
* [Cloudflare Turnstile](https://www.cloudflare.com/application-services/products/turnstile/)
* [hCaptcha](https://www.hcaptcha.com/)
This plugin works out of the box with Email & Password authentication. To use it with other authentication methods, you will need to configure the endpoints array in the plugin options.
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { captcha } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [ // [!code highlight]
captcha({ // [!code highlight]
provider: "cloudflare-turnstile", // or google-recaptcha, hcaptcha // [!code highlight]
secretKey: process.env.TURNSTILE_SECRET_KEY!, // [!code highlight]
}), // [!code highlight]
], // [!code highlight]
});
```
### Add the captcha token to your request headers
Add the captcha token to your request headers for all protected endpoints. This example shows how to include it in a `signIn` request:
```ts
await authClient.signIn.email({
email: "user@example.com",
password: "secure-password",
fetchOptions: { // [!code highlight]
headers: { // [!code highlight]
"x-captcha-response": turnstileToken, // [!code highlight]
"x-captcha-user-remote-ip": userIp, // optional: forwards the user's IP address to the captcha service // [!code highlight]
}, // [!code highlight]
}, // [!code highlight]
});
```
* To implement Cloudflare Turnstile on the client side, follow the official [Cloudflare Turnstile documentation](https://developers.cloudflare.com/turnstile/) or use a library like [react-turnstile](https://www.npmjs.com/package/@marsidev/react-turnstile).
* To implement Google reCAPTCHA on the client side, follow the official [Google reCAPTCHA documentation](https://developers.google.com/recaptcha/intro) or use libraries like [react-google-recaptcha](https://www.npmjs.com/package/react-google-recaptcha) (v2) and [react-google-recaptcha-v3](https://www.npmjs.com/package/react-google-recaptcha-v3) (v3).
* To implement hCaptcha on the client side, follow the official [hCaptcha documentation](https://docs.hcaptcha.com/#add-the-hcaptcha-widget-to-your-webpage) or use libraries like [@hcaptcha/react-hcaptcha](https://www.npmjs.com/package/@hcaptcha/react-hcaptcha)
## How it works
The plugin acts as a middleware: it intercepts all `POST` requests to configured endpoints (see `endpoints`
in the [Plugin Options](#plugin-options) section).
it validates the captcha token on the server, by calling the captcha provider's `/siteverify`.
* if the token is missing, gets rejected by the captcha provider, or if the `/siteverify` endpoint is
unavailable, the plugin returns an error and interrupts the request.
* if the token is accepted by the captcha provider, the middleware returns `undefined`, meaning the request is allowed to proceed.
## Plugin Options
* **`provider` (required)**: your captcha provider.
* **`secretKey` (required)**: your provider's secret key used for the server-side validation.
* `endpoints` (optional): overrides the default array of paths where captcha validation is enforced. Default is: `["/sign-up/email", "/sign-in/email", "/forget-password",]`.
* `minScore` (optional - only *Google ReCAPTCHA v3*): minimum score threshold. Default is `0.5`.
* `siteKey` (optional - only *hCaptcha*): prevents tokens issued on one sitekey from being redeemed elsewhere.
* `siteVerifyURLOverride` (optional): overrides endpoint URL for the captcha verification request.
# plugins: Community Plugins
URL: /docs/plugins/community-plugins
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/community-plugins.mdx
A list of recommended community plugins.
***
title: Community Plugins
description: A list of recommended community plugins.
-----------------------------------------------------
This page showcases a list of recommended community made plugins.
We encourage you to create custom plugins and maybe get added to the list!
To create your own custom plugin, get started by reading our [plugins documentation](/docs/concepts/plugins). And if you want to share your plugin with the community, please open a pull request to add it to this list.
|
Plugin
| Description |
Author
|
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [better-auth-harmony](https://github.com/gekorm/better-auth-harmony/) | Email & phone normalization and additional validation, blocking over 55,000 temporary email domains. | [GeKorm](https://github.com/GeKorm) |
| [validation-better-auth](https://github.com/Daanish2003/validation-better-auth) | Validate API request using any validation library (e.g., Zod, Yup) | [Daanish2003](https://github.com/Daanish2003) |
| [better-auth-localization](https://github.com/marcellosso/better-auth-localization) | Localize and customize better-auth messages with easy translation and message override support. | [marcellosso](https://github.com/marcellosso) |
| [better-auth-attio-plugin](https://github.com/tobimori/better-auth-attio-plugin) | Sync your products Better Auth users & workspaces with Attio | [tobimori](https://github.com/tobimori) |
| [better-auth-cloudflare](https://github.com/zpg6/better-auth-cloudflare) | Seamlessly integrate with Cloudflare Workers, D1, Hyperdrive, KV, R2, and geolocation services. Includes CLI for project generation, automated resource provisioning on Cloudflare, and database migrations. Supports Next.js, Hono, and more! | [zpg6](https://github.com/zpg6) |
| [expo-better-auth-passkey](https://github.com/kevcube/expo-better-auth-passkey) | Better-auth client plugin for using passkeys on mobile platforms in expo apps. Supports iOS, macOS, Android (and web!) by wrapping the existing better-auth passkey client plugin. | [kevcube](https://github.comkevcube) |
# plugins: Device Authorization
URL: /docs/plugins/device-authorization
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/device-authorization.mdx
OAuth 2.0 Device Authorization Grant for limited-input devices
***
title: Device Authorization
description: OAuth 2.0 Device Authorization Grant for limited-input devices
---------------------------------------------------------------------------
`RFC 8628` `CLI` `Smart TV` `IoT`
The Device Authorization plugin implements the OAuth 2.0 Device Authorization Grant ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)), enabling authentication for devices with limited input capabilities such as smart TVs, CLI applications, IoT devices, and gaming consoles.
## Try It Out
You can test the device authorization flow right now using the Better Auth CLI:
```bash
npx @better-auth/cli login
```
This will demonstrate the complete device authorization flow by:
1. Requesting a device code from the Better Auth demo server
2. Displaying a user code for you to enter
3. Opening your browser to the verification page
4. Polling for authorization completion
The CLI login command is a demo feature that connects to the Better Auth demo server to showcase the device authorization flow in action.
## Installation
### Add the plugin to your auth config
Add the device authorization plugin to your server configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { deviceAuthorization } from "better-auth/plugins"; // [!code highlight]
export const auth = betterAuth({
// ... other config
plugins: [ // [!code highlight]
deviceAuthorization({ // [!code highlight]
// Optional configuration
expiresIn: "30m", // Device code expiration time // [!code highlight]
interval: "5s", // Minimum polling interval // [!code highlight]
}), // [!code highlight]
], // [!code highlight]
});
```
### Migrate the database
Run the migration or generate the schema to add the necessary tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
Add the device authorization plugin to your client.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins"; // [!code highlight]
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
deviceAuthorizationClient(), // [!code highlight]
], // [!code highlight]
});
```
## How It Works
The device flow follows these steps:
1. **Device requests codes**: The device requests a device code and user code from the authorization server
2. **User authorizes**: The user visits a verification URL and enters the user code
3. **Device polls for token**: The device polls the server until the user completes authorization
4. **Access granted**: Once authorized, the device receives an access token
## Basic Usage
### Requesting Device Authorization
To initiate device authorization, call `device.code` with the client ID:
### Client Side
```ts
const { data, error } = await authClient.device.code({
client_id,
scope, // required
});
```
### Server Side
```ts
const data = await auth.api.deviceCode({
body: {
client_id,
scope, // required
}
});
```
### Type Definition
```ts
type deviceCode = {
/**
* The OAuth client identifier
*/
client_id: string;
/**
* Space-separated list of requested scopes (optional)
*/
scope?: string;
}
```
Example usage:
```ts
const { data } = await authClient.device.code({
client_id: "your-client-id",
scope: "openid profile email",
});
if (data) {
console.log(`Please visit: ${data.verification_uri}`);
console.log(`And enter code: ${data.user_code}`);
}
```
### Polling for Token
After displaying the user code, poll for the access token:
### Client Side
```ts
const { data, error } = await authClient.device.token({
grant_type,
device_code,
client_id,
});
```
### Server Side
```ts
const data = await auth.api.deviceToken({
body: {
grant_type,
device_code,
client_id,
}
});
```
### Type Definition
```ts
type deviceToken = {
/**
* Must be "urn:ietf:params:oauth:grant-type:device_code"
*/
grant_type: string;
/**
* The device code from the initial request
*/
device_code: string;
/**
* The OAuth client identifier
*/
client_id: string;
}
```
Example polling implementation:
```ts
let pollingInterval = 5; // Start with 5 seconds
const pollForToken = async () => {
const { data, error } = await authClient.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code,
client_id: yourClientId,
fetchOptions: {
headers: {
"user-agent": `My CLI`,
},
},
});
if (data?.access_token) {
console.log("Authorization successful!");
} else if (error) {
switch (error.error) {
case "authorization_pending":
// Continue polling
break;
case "slow_down":
pollingInterval += 5;
break;
case "access_denied":
console.error("Access was denied by the user");
return;
case "expired_token":
console.error("The device code has expired. Please try again.");
return;
default:
console.error(`Error: ${error.error_description}`);
return;
}
setTimeout(pollForToken, pollingInterval * 1000);
}
};
pollForToken();
```
### User Authorization Flow
The user authorization flow requires two steps:
1. **Code Verification**: Check if the entered user code is valid
2. **Authorization**: User must be authenticated to approve/deny the device
Users must be authenticated before they can approve or deny device authorization requests. If not authenticated, redirect them to the login page with a return URL.
Create a page where users can enter their code:
```tsx title="app/device/page.tsx"
export default function DeviceAuthorizationPage() {
const [userCode, setUserCode] = useState("");
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Format the code: remove dashes and convert to uppercase
const formattedCode = userCode.trim().replace(/-/g, "").toUpperCase();
// Check if the code is valid using GET /device endpoint
const response = await authClient.device.deviceVerify({
query: { user_code: formattedCode },
});
if (response.data) {
// Redirect to approval page
window.location.href = `/device/approve?user_code=${formattedCode}`;
}
} catch (err) {
setError("Invalid or expired code");
}
};
return (
);
}
```
### Approving or Denying Device
Users must be authenticated to approve or deny device authorization requests:
#### Approve Device
### Client Side
```ts
const { data, error } = await authClient.device.approve({
userCode,
});
```
### Server Side
```ts
const data = await auth.api.deviceApprove({
body: {
userCode,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type deviceApprove = {
/**
* The user code to approve
*/
userCode: string;
}
```
#### Deny Device
### Client Side
```ts
const { data, error } = await authClient.device.deny({
userCode,
});
```
### Server Side
```ts
const data = await auth.api.deviceDeny({
body: {
userCode,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type deviceDeny = {
/**
* The user code to deny
*/
userCode: string;
}
```
#### Example Approval Page
```tsx title="app/device/approve/page.tsx"
export default function DeviceApprovalPage() {
const { user } = useAuth(); // Must be authenticated
const searchParams = useSearchParams();
const userCode = searchParams.get("userCode");
const [isProcessing, setIsProcessing] = useState(false);
const handleApprove = async () => {
setIsProcessing(true);
try {
await authClient.device.deviceApprove({
userCode: userCode,
});
// Show success message
alert("Device approved successfully!");
window.location.href = "/";
} catch (error) {
alert("Failed to approve device");
}
setIsProcessing(false);
};
const handleDeny = async () => {
setIsProcessing(true);
try {
await authClient.device.deviceDeny({
userCode: userCode,
});
alert("Device denied");
window.location.href = "/";
} catch (error) {
alert("Failed to deny device");
}
setIsProcessing(false);
};
if (!user) {
// Redirect to login if not authenticated
window.location.href = `/login?redirect=/device/approve?user_code=${userCode}`;
return null;
}
return (
Device Authorization Request
A device is requesting access to your account.
Code: {userCode}
);
}
```
## Advanced Configuration
### Client Validation
You can validate client IDs to ensure only authorized applications can use the device flow:
```ts
deviceAuthorization({
validateClient: async (clientId) => {
// Check if client is authorized
const client = await db.oauth_clients.findOne({ id: clientId });
return client && client.allowDeviceFlow;
},
onDeviceAuthRequest: async (clientId, scope) => {
// Log device authorization requests
await logDeviceAuthRequest(clientId, scope);
},
})
```
### Custom Code Generation
Customize how device and user codes are generated:
```ts
deviceAuthorization({
generateDeviceCode: async () => {
// Custom device code generation
return crypto.randomBytes(32).toString("hex");
},
generateUserCode: async () => {
// Custom user code generation
// Default uses: ABCDEFGHJKLMNPQRSTUVWXYZ23456789
// (excludes 0, O, 1, I to avoid confusion)
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "";
for (let i = 0; i < 8; i++) {
code += charset[Math.floor(Math.random() * charset.length)];
}
return code;
},
})
```
## Error Handling
The device flow defines specific error codes:
| Error Code | Description |
| ----------------------- | ------------------------------------------- |
| `authorization_pending` | User hasn't approved yet (continue polling) |
| `slow_down` | Polling too frequently (increase interval) |
| `expired_token` | Device code has expired |
| `access_denied` | User denied the authorization |
| `invalid_grant` | Invalid device code or client ID |
## Example: CLI Application
Here's a complete example for a CLI application based on the actual demo:
```ts title="cli-auth.ts"
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins";
import open from "open";
const authClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [deviceAuthorizationClient()],
});
async function authenticateCLI() {
console.log("🔐 Better Auth Device Authorization Demo");
console.log("⏳ Requesting device authorization...");
try {
// Request device code
const { data, error } = await authClient.device.code({
client_id: "demo-cli",
scope: "openid profile email",
});
if (error || !data) {
console.error("❌ Error:", error?.error_description);
process.exit(1);
}
const {
device_code,
user_code,
verification_uri,
verification_uri_complete,
interval = 5,
} = data;
console.log("\n📱 Device Authorization in Progress");
console.log(`Please visit: ${verification_uri}`);
console.log(`Enter code: ${user_code}\n`);
// Open browser with the complete URL
const urlToOpen = verification_uri_complete || verification_uri;
if (urlToOpen) {
console.log("🌐 Opening browser...");
await open(urlToOpen);
}
console.log(`⏳ Waiting for authorization... (polling every ${interval}s)`);
// Poll for token
await pollForToken(device_code, interval);
} catch (err) {
console.error("❌ Error:", err.message);
process.exit(1);
}
}
async function pollForToken(deviceCode: string, interval: number) {
let pollingInterval = interval;
return new Promise((resolve) => {
const poll = async () => {
try {
const { data, error } = await authClient.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: deviceCode,
client_id: "demo-cli",
});
if (data?.access_token) {
console.log("\nAuthorization Successful!");
console.log("Access token received!");
// Get user session
const { data: session } = await authClient.getSession({
fetchOptions: {
headers: {
Authorization: `Bearer ${data.access_token}`,
},
},
});
console.log(`Hello, ${session?.user?.name || "User"}!`);
resolve();
process.exit(0);
} else if (error) {
switch (error.error) {
case "authorization_pending":
// Continue polling silently
break;
case "slow_down":
pollingInterval += 5;
console.log(`⚠️ Slowing down polling to ${pollingInterval}s`);
break;
case "access_denied":
console.error("❌ Access was denied by the user");
process.exit(1);
break;
case "expired_token":
console.error("❌ The device code has expired. Please try again.");
process.exit(1);
break;
default:
console.error("❌ Error:", error.error_description);
process.exit(1);
}
}
} catch (err) {
console.error("❌ Network error:", err.message);
process.exit(1);
}
// Schedule next poll
setTimeout(poll, pollingInterval * 1000);
};
// Start polling
setTimeout(poll, pollingInterval * 1000);
});
}
// Run the authentication flow
authenticateCLI().catch((err) => {
console.error("❌ Fatal error:", err);
process.exit(1);
});
```
## Security Considerations
1. **Rate Limiting**: The plugin enforces polling intervals to prevent abuse
2. **Code Expiration**: Device and user codes expire after the configured time (default: 30 minutes)
3. **Client Validation**: Always validate client IDs in production to prevent unauthorized access
4. **HTTPS Only**: Always use HTTPS in production for device authorization
5. **User Code Format**: User codes use a limited character set (excluding similar-looking characters like 0/O, 1/I) to reduce typing errors
6. **Authentication Required**: Users must be authenticated before they can approve or deny device requests
## Options
### Server
**expiresIn**: The expiration time for device codes. Default: `"30m"` (30 minutes).
**interval**: The minimum polling interval. Default: `"5s"` (5 seconds).
**userCodeLength**: The length of the user code. Default: `8`.
**deviceCodeLength**: The length of the device code. Default: `40`.
**generateDeviceCode**: Custom function to generate device codes. Returns a string or `Promise`.
**generateUserCode**: Custom function to generate user codes. Returns a string or `Promise`.
**validateClient**: Function to validate client IDs. Takes a clientId and returns boolean or `Promise`.
**onDeviceAuthRequest**: Hook called when device authorization is requested. Takes clientId and optional scope.
### Client
No client-specific configuration options. The plugin adds the following methods:
* **device.code()**: Request device and user codes
* **device.token()**: Poll for access token
* **device.deviceVerify()**: Verify user code validity
* **device.deviceApprove()**: Approve device (requires authentication)
* **device.deviceDeny()**: Deny device (requires authentication)
## Schema
The plugin requires a new table to store device authorization data.
Table Name: `deviceCode`
# plugins: Dodo Payments
URL: /docs/plugins/dodopayments
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/dodopayments.mdx
Better Auth Plugin for Dodo Payments
***
title: Dodo Payments
description: Better Auth Plugin for Dodo Payments
-------------------------------------------------
[Dodo Payments](https://dodopayments.com) is a global Merchant-of-Record platform that lets AI, SaaS and digital businesses sell in 150+ countries without touching tax, fraud, or compliance. A single, developer-friendly API powers checkout, billing, and payouts so you can launch worldwide in minutes.
This plugin is maintained by the Dodo Payments team.
Have questions? Our team is available on Discord to assist you anytime.
## Features
* Automatic customer creation on sign-up
* Type-safe checkout flows with product slug mapping
* Self-service customer portal
* Real-time webhook event processing with signature verification
You need a Dodo Payments account and API keys to use this integration.
## Installation
Run the following command in your project root:
```bash
npm install @dodopayments/better-auth dodopayments better-auth zod
```
Add these to your `.env` file:
```txt
DODO_PAYMENTS_API_KEY=your_api_key_here
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
```
Create or update `src/lib/auth.ts`:
```typescript
import { betterAuth } from "better-auth";
import {
dodopayments,
checkout,
portal,
webhooks,
} from "@dodopayments/better-auth";
import DodoPayments from "dodopayments";
export const dodoPayments = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
environment: "test_mode"
});
export const auth = betterAuth({
plugins: [
dodopayments({
client: dodoPayments,
createCustomerOnSignUp: true,
use: [
checkout({
products: [
{
productId: "pdt_xxxxxxxxxxxxxxxxxxxxx",
slug: "premium-plan",
},
],
successUrl: "/dashboard/success",
authenticatedUsersOnly: true,
}),
portal(),
webhooks({
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!,
onPayload: async (payload) => {
console.log("Received webhook:", payload.event_type);
},
}),
],
}),
],
});
```
Set `environment` to `live_mode` for production.
Create or update `src/lib/auth-client.ts`:
```typescript
import { dodopaymentsClient } from "@dodopayments/better-auth";
export const authClient = createAuthClient({
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
plugins: [dodopaymentsClient()],
});
```
## Usage
### Creating a Checkout Session
```typescript
const { data: checkout, error } = await authClient.dodopayments.checkout({
slug: "premium-plan",
customer: {
email: "customer@example.com",
name: "John Doe",
},
billing: {
city: "San Francisco",
country: "US",
state: "CA",
street: "123 Market St",
zipcode: "94103",
},
referenceId: "order_123",
});
if (checkout) {
window.location.href = checkout.url;
}
```
### Accessing the Customer Portal
```typescript
const { data: customerPortal, error } = await authClient.dodopayments.customer.portal();
if (customerPortal && customerPortal.redirect) {
window.location.href = customerPortal.url;
}
```
### Listing Customer Data
```typescript
// Get subscriptions
const { data: subscriptions, error } =
await authClient.dodopayments.customer.subscriptions.list({
query: {
limit: 10,
page: 1,
active: true,
},
});
// Get payment history
const { data: payments, error } = await authClient.dodopayments.customer.payments.list({
query: {
limit: 10,
page: 1,
status: "succeeded",
},
});
```
### Webhooks
The webhooks plugin processes real-time payment events from Dodo Payments with secure signature verification. The default endpoint is `/api/auth/dodopayments/webhooks`.
Generate a webhook secret for your endpoint URL (e.g., `https://your-domain.com/api/auth/dodopayments/webhooks`) in the Dodo Payments Dashboard and set it in your .env file:
```txt
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
```
Example handler:
```typescript
webhooks({
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!,
onPayload: async (payload) => {
console.log("Received webhook:", payload.event_type);
},
});
```
## Configuration Reference
### Plugin Options
* **client** (required): DodoPayments client instance
* **createCustomerOnSignUp** (optional): Auto-create customers on user signup
* **use** (required): Array of plugins to enable (checkout, portal, webhooks)
### Checkout Plugin Options
* **products**: Array of products or async function returning products
* **successUrl**: URL to redirect after successful payment
* **authenticatedUsersOnly**: Require user authentication (default: false)
If you encounter any issues, please refer to the [Dodo Payments documentation](https://docs.dodopayments.com) for troubleshooting steps.
# plugins: Dub
URL: /docs/plugins/dub
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/dub.mdx
Better Auth Plugin for Lead Tracking using Dub links and OAuth Linking
***
title: Dub
description: Better Auth Plugin for Lead Tracking using Dub links and OAuth Linking
-----------------------------------------------------------------------------------
[Dub](https://dub.co/) is an open source modern link management platform for entrepreneurs, creators, and growth teams.
This plugins allows you to track leads when a user signs up using a Dub link. It also adds OAuth linking support to allow you to build integrations extending Dub's linking management infrastructure.
## Installation
### Install the plugin
First, install the plugin:
npm
pnpm
yarn
bun
```bash
npm install @dub/better-auth
```
```bash
pnpm add @dub/better-auth
```
```bash
yarn add @dub/better-auth
```
```bash
bun add @dub/better-auth
```
### Install the Dub SDK
Next, install the Dub SDK on your server:
npm
pnpm
yarn
bun
```bash
npm install dub
```
```bash
pnpm add dub
```
```bash
yarn add dub
```
```bash
bun add dub
```
### Configure the plugin
Add the plugin to your auth config:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { dubAnalytics } from "@dub/better-auth"
import { dub } from "dub"
export const auth = betterAuth({
plugins: [
dubAnalytics({
dubClient: new Dub()
})
]
})
```
## Usage
### Lead Tracking
By default, the plugin will track sign up events as leads. You can disable this by setting `disableLeadTracking` to `true`.
```ts
import { dubAnalytics } from "@dub/better-auth";
import { betterAuth } from "better-auth";
import { Dub } from "dub";
const dub = new Dub();
const betterAuth = betterAuth({
plugins: [
dubAnalytics({
dubClient: dub,
disableLeadTracking: true, // Disable lead tracking
}),
],
});
```
### OAuth Linking
The plugin supports OAuth for account linking.
First, you need to setup OAuth app in Dub. Dub supports OAuth 2.0 authentication, which is recommended if you build integrations extending Dub’s functionality [Learn more about OAuth](https://dub.co/docs/integrations/quickstart#integrating-via-oauth-2-0-recommended).
Once you get the client ID and client secret, you can configure the plugin.
```ts
dubAnalytics({
dubClient: dub,
oauth: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
},
});
```
And in the client, you need to use the `dubAnalyticsClient` plugin.
```ts
import { createAuthClient } from "better-auth/client";
import { dubAnalyticsClient } from "@dub/better-auth/client";
const authClient = createAuthClient({
plugins: [dubAnalyticsClient()],
});
```
To link account with Dub, you need to use the `dub.link`.
### Client Side
```ts
const { data, error } = await authClient.dub.link({
callbackURL: /dashboard,
});
```
### Server Side
```ts
const data = await auth.api.dubLink({
body: {
callbackURL: /dashboard,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type dubLink = {
/**
* URL to redirect to after linking
* @clientOnly
*/
callbackURL: string = "/dashboard"
}
```
## Options
You can pass the following options to the plugin:
### `dubClient`
The Dub client instance.
### `disableLeadTracking`
Disable lead tracking for sign up events.
### `leadEventName`
Event name for sign up leads.
### `customLeadTrack`
Custom lead track function.
### `oauth`
Dub OAuth configuration.
### `oauth.clientId`
Client ID for Dub OAuth.
### `oauth.clientSecret`
Client secret for Dub OAuth.
### `oauth.pkce`
Enable PKCE for Dub OAuth.
# plugins: Email OTP
URL: /docs/plugins/email-otp
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/email-otp.mdx
Email OTP plugin for Better Auth.
***
title: Email OTP
description: Email OTP plugin for Better Auth.
----------------------------------------------
The Email OTP plugin allows user to sign in, verify their email, or reset their password using a one-time password (OTP) sent to their email address.
## Installation
### Add the plugin to your auth config
Add the `emailOTP` plugin to your auth config and implement the `sendVerificationOTP()` method.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
emailOTP({ // [!code highlight]
async sendVerificationOTP({ email, otp, type }) { // [!code highlight]
if (type === "sign-in") { // [!code highlight]
// Send the OTP for sign in // [!code highlight]
} else if (type === "email-verification") { // [!code highlight]
// Send the OTP for email verification // [!code highlight]
} else { // [!code highlight]
// Send the OTP for password reset // [!code highlight]
} // [!code highlight]
}, // [!code highlight]
}) // [!code highlight]
]
})
```
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { emailOTPClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
emailOTPClient()
]
})
```
## Usage
### Send an OTP
Use the `sendVerificationOtp()` method to send an OTP to the user's email address.
### Client Side
```ts
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
email: user@example.com,
type: sign-in,
});
```
### Server Side
```ts
const data = await auth.api.sendVerificationOTP({
body: {
email: user@example.com,
type: sign-in,
}
});
```
### Type Definition
```ts
type sendVerificationOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
/**
* Type of the OTP. `sign-in`, `email-verification`, or `forget-password`.
*/
type: "email-verification" | "sign-in" | "forget-password" = "sign-in"
}
```
### Check an OTP (optional)
Use the `checkVerificationOtp()` method to check if an OTP is valid.
### Client Side
```ts
const { data, error } = await authClient.emailOtp.checkVerificationOtp({
email: user@example.com,
type: sign-in,
otp: 123456,
});
```
### Server Side
```ts
const data = await auth.api.checkVerificationOTP({
body: {
email: user@example.com,
type: sign-in,
otp: 123456,
}
});
```
### Type Definition
```ts
type checkVerificationOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
/**
* Type of the OTP. `sign-in`, `email-verification`, or `forget-password`.
*/
type: "email-verification" | "sign-in" | "forget-password" = "sign-in"
/**
* OTP sent to the email.
*/
otp: string = "123456"
}
```
### Sign In with OTP
To sign in with OTP, use the `sendVerificationOtp()` method to send a "sign-in" OTP to the user's email address.
### Client Side
```ts
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
email: user@example.com,
type: sign-in,
});
```
### Server Side
```ts
const data = await auth.api.sendVerificationOTP({
body: {
email: user@example.com,
type: sign-in,
}
});
```
### Type Definition
```ts
type sendVerificationOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
/**
* Type of the OTP.
*/
type: "sign-in" = "sign-in"
}
```
Once the user provides the OTP, you can sign in the user using the `signIn.emailOtp()` method.
### Client Side
```ts
const { data, error } = await authClient.signIn.emailOtp({
email: user@example.com,
otp: 123456,
});
```
### Server Side
```ts
const data = await auth.api.signInEmailOTP({
body: {
email: user@example.com,
otp: 123456,
}
});
```
### Type Definition
```ts
type signInEmailOTP = {
/**
* Email address to sign in.
*/
email: string = "user@example.com"
/**
* OTP sent to the email.
*/
otp: string = "123456"
}
```
If the user is not registered, they'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the [options](#options).
### Verify Email with OTP
To verify the user's email address with OTP, use the `sendVerificationOtp()` method to send an "email-verification" OTP to the user's email address.
### Client Side
```ts
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
email: user@example.com,
type: email-verification,
});
```
### Server Side
```ts
const data = await auth.api.sendVerificationOTP({
body: {
email: user@example.com,
type: email-verification,
}
});
```
### Type Definition
```ts
type sendVerificationOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
/**
* Type of the OTP.
*/
type: "email-verification" = "email-verification"
}
```
Once the user provides the OTP, use the `verifyEmail()` method to complete email verification.
### Client Side
```ts
const { data, error } = await authClient.emailOtp.verifyEmail({
email: user@example.com,
otp: 123456,
});
```
### Server Side
```ts
const data = await auth.api.verifyEmailOTP({
body: {
email: user@example.com,
otp: 123456,
}
});
```
### Type Definition
```ts
type verifyEmailOTP = {
/**
* Email address to verify.
*/
email: string = "user@example.com"
/**
* OTP to verify.
*/
otp: string = "123456"
}
```
### Reset Password with OTP
To reset the user's password with OTP, use the `forgetPassword.emailOTP()` method to send a "forget-password" OTP to the user's email address.
### Client Side
```ts
const { data, error } = await authClient.forgetPassword.emailOtp({
email: user@example.com,
});
```
### Server Side
```ts
const data = await auth.api.forgetPasswordEmailOTP({
body: {
email: user@example.com,
}
});
```
### Type Definition
```ts
type forgetPasswordEmailOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
}
```
Once the user provides the OTP, use the `checkVerificationOtp()` method to check if it's valid (optional).
### Client Side
```ts
const { data, error } = await authClient.emailOtp.checkVerificationOtp({
email: user@example.com,
type: forget-password,
otp: 123456,
});
```
### Server Side
```ts
const data = await auth.api.checkVerificationOTP({
body: {
email: user@example.com,
type: forget-password,
otp: 123456,
}
});
```
### Type Definition
```ts
type checkVerificationOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
/**
* Type of the OTP.
*/
type: "forget-password" = "forget-password"
/**
* OTP sent to the email.
*/
otp: string = "123456"
}
```
Then, use the `resetPassword()` method to reset the user's password.
### Client Side
```ts
const { data, error } = await authClient.emailOtp.resetPassword({
email: user@example.com,
otp: 123456,
password: new-secure-password,
});
```
### Server Side
```ts
const data = await auth.api.resetPasswordEmailOTP({
body: {
email: user@example.com,
otp: 123456,
password: new-secure-password,
}
});
```
### Type Definition
```ts
type resetPasswordEmailOTP = {
/**
* Email address to reset the password.
*/
email: string = "user@example.com"
/**
* OTP sent to the email.
*/
otp: string = "123456"
/**
* New password.
*/
password: string = "new-secure-password"
}
```
### Override Default Email Verification
To override the default email verification, pass `overrideDefaultEmailVerification: true` in the options. This will make the system use an email OTP instead of the default verification link whenever email verification is triggered. In other words, the user will verify their email using an OTP rather than clicking a link.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [
emailOTP({
overrideDefaultEmailVerification: true, // [!code highlight]
async sendVerificationOTP({ email, otp, type }) {
// Implement the sendVerificationOTP method to send the OTP to the user's email address
},
}),
],
});
```
## Options
* `sendVerificationOTP`: A function that sends the OTP to the user's email address. The function receives an object with the following properties:
* `email`: The user's email address.
* `otp`: The OTP to send.
* `type`: The type of OTP to send. Can be "sign-in", "email-verification", or "forget-password".
* `otpLength`: The length of the OTP. Defaults to `6`.
* `expiresIn`: The expiry time of the OTP in seconds. Defaults to `300` seconds.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
emailOTP({
otpLength: 8,
expiresIn: 600
})
]
})
```
* `sendVerificationOnSignUp`: A boolean value that determines whether to send the OTP when a user signs up. Defaults to `false`.
* `disableSignUp`: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults to `false`.
* `generateOTP`: A function that generates the OTP. Defaults to a random 6-digit number.
* `allowedAttempts`: The maximum number of attempts allowed for verifying an OTP. Defaults to `3`. After exceeding this limit, the OTP becomes invalid and the user needs to request a new one.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
emailOTP({
allowedAttempts: 5, // Allow 5 attempts before invalidating the OTP
expiresIn: 300
})
]
})
```
When the maximum attempts are exceeded, the `verifyOTP`, `signIn.emailOtp`, `verifyEmail`, and `resetPassword` methods will return an error with code `TOO_MANY_ATTEMPTS`.
* `storeOTP`: The method to store the OTP in your database, wether `encrypted`, `hashed` or `plain` text. Default is `plain` text.
Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database.
Alternatively, you can pass a custom encryptor or hasher to store the OTP in your database.
**Custom encryptor**
```ts title="auth.ts"
emailOTP({
storeOTP: {
encrypt: async (otp) => {
return myCustomEncryptor(otp);
},
decrypt: async (otp) => {
return myCustomDecryptor(otp);
},
}
})
```
**Custom hasher**
```ts title="auth.ts"
emailOTP({
storeOTP: {
hash: async (otp) => {
return myCustomHasher(otp);
},
}
})
```
# plugins: Generic OAuth
URL: /docs/plugins/generic-oauth
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/generic-oauth.mdx
Authenticate users with any OAuth provider
***
title: Generic OAuth
description: Authenticate users with any OAuth provider
-------------------------------------------------------
The Generic OAuth plugin provides a flexible way to integrate authentication with any OAuth provider. It supports both OAuth 2.0 and OpenID Connect (OIDC) flows, allowing you to easily add social login or custom OAuth authentication to your application.
## Installation
### Add the plugin to your auth config
To use the Generic OAuth plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { genericOAuth } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
genericOAuth({ // [!code highlight]
config: [ // [!code highlight]
{ // [!code highlight]
providerId: "provider-id", // [!code highlight]
clientId: "test-client-id", // [!code highlight]
clientSecret: "test-client-secret", // [!code highlight]
discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight]
// ... other config options // [!code highlight]
}, // [!code highlight]
// Add more providers as needed // [!code highlight]
] // [!code highlight]
}) // [!code highlight]
]
})
```
### Add the client plugin
Include the Generic OAuth client plugin in your authentication client instance.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { genericOAuthClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
genericOAuthClient()
]
})
```
## Usage
The Generic OAuth plugin provides endpoints for initiating the OAuth flow and handling the callback. Here's how to use them:
### Initiate OAuth Sign-In
To start the OAuth sign-in process:
### Client Side
```ts
const { data, error } = await authClient.signIn.oauth2({
providerId: provider-id,
callbackURL: /dashboard, // required
errorCallbackURL: /error-page, // required
newUserCallbackURL: /welcome, // required
disableRedirect, // required
scopes, // required
requestSignUp, // required
});
```
### Server Side
```ts
const data = await auth.api.signInWithOAuth2({
body: {
providerId: provider-id,
callbackURL: /dashboard, // required
errorCallbackURL: /error-page, // required
newUserCallbackURL: /welcome, // required
disableRedirect, // required
scopes, // required
requestSignUp, // required
}
});
```
### Type Definition
```ts
type signInWithOAuth2 = {
/**
* The provider ID for the OAuth provider.
*/
providerId: string = "provider-id"
/**
* The URL to redirect to after sign in.
*/
callbackURL?: string = "/dashboard"
/**
* The URL to redirect to if an error occurs.
*/
errorCallbackURL?: string = "/error-page"
/**
* The URL to redirect to after login if the user is new.
*/
newUserCallbackURL?: string = "/welcome"
/**
* Disable redirect.
*/
disableRedirect?: boolean = false
/**
* Scopes to be passed to the provider authorization request.
*/
scopes?: string[] = ["my-scope"]
/**
* Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider.
*/
requestSignUp?: boolean = false
}
```
### Linking OAuth Accounts
To link an OAuth account to an existing user:
### Client Side
```ts
const { data, error } = await authClient.oauth2.link({
providerId: my-provider-id,
callbackURL: /successful-link,
});
```
### Server Side
```ts
const data = await auth.api.oAuth2LinkAccount({
body: {
providerId: my-provider-id,
callbackURL: /successful-link,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type oAuth2LinkAccount = {
/**
* The OAuth provider ID.
*/
providerId: string = "my-provider-id"
/**
* The URL to redirect to once the account linking was complete.
*/
callbackURL: string = "/successful-link"
}
```
### Handle OAuth Callback
The plugin mounts a route to handle the OAuth callback `/oauth2/callback/:providerId`. This means by default `${baseURL}/api/auth/oauth2/callback/:providerId` will be used as the callback URL. Make sure your OAuth provider is configured to use this URL.
## Configuration
When adding the plugin to your auth config, you can configure multiple OAuth providers. Each provider configuration object supports the following options:
```ts
interface GenericOAuthConfig {
providerId: string;
discoveryUrl?: string;
authorizationUrl?: string;
tokenUrl?: string;
userInfoUrl?: string;
clientId: string;
clientSecret: string;
scopes?: string[];
redirectURI?: string;
responseType?: string;
prompt?: string;
pkce?: boolean;
accessType?: string;
getUserInfo?: (tokens: OAuth2Tokens) => Promise;
}
```
### Other Provider Configurations
**providerId**: A unique string to identify the OAuth provider configuration.
**discoveryUrl**: (Optional) URL to fetch the provider's OAuth 2.0/OIDC configuration. If provided, endpoints like `authorizationUrl`, `tokenUrl`, and `userInfoUrl` can be auto-discovered.
**authorizationUrl**: (Optional) The OAuth provider's authorization endpoint. Not required if using `discoveryUrl`.
**tokenUrl**: (Optional) The OAuth provider's token endpoint. Not required if using `discoveryUrl`.
**userInfoUrl**: (Optional) The endpoint to fetch user profile information. Not required if using `discoveryUrl`.
**clientId**: The OAuth client ID issued by your provider.
**clientSecret**: The OAuth client secret issued by your provider.
**scopes**: (Optional) An array of scopes to request from the provider (e.g., `["openid", "email", "profile"]`).
**redirectURI**: (Optional) The redirect URI to use for the OAuth flow. If not set, a default is constructed based on your app's base URL.
**responseType**: (Optional) The OAuth response type. Defaults to `"code"` for authorization code flow.
**responseMode**: (Optional) The response mode for the authorization code request, such as `"query"` or `"form_post"`.
**prompt**: (Optional) Controls the authentication experience (e.g., force login, consent, etc.).
**pkce**: (Optional) If true, enables PKCE (Proof Key for Code Exchange) for enhanced security. Defaults to `false`.
**accessType**: (Optional) The access type for the authorization request. Use `"offline"` to request a refresh token.
**getUserInfo**: (Optional) A custom function to fetch user info from the provider, given the OAuth tokens. If not provided, a default fetch is used.
**mapProfileToUser**: (Optional) A function to map the provider's user profile to your app's user object. Useful for custom field mapping or transformations.
**authorizationUrlParams**: (Optional) Additional query parameters to add to the authorization URL. These can override default parameters.
**disableImplicitSignUp**: (Optional) If true, disables automatic sign-up for new users. Sign-in must be explicitly requested with sign-up intent.
**disableSignUp**: (Optional) If true, disables sign-up for new users entirely. Only existing users can sign in.
**authentication**: (Optional) The authentication method for token requests. Can be `'basic'` or `'post'`. Defaults to `'post'`.
**discoveryHeaders**: (Optional) Custom headers to include in the discovery request. Useful for providers that require special headers.
**authorizationHeaders**: (Optional) Custom headers to include in the authorization request. Useful for providers that require special headers.
**overrideUserInfo**: (Optional) If true, the user's info in your database will be updated with the provider's info every time they sign in. Defaults to `false`.
## Advanced Usage
### Custom User Info Fetching
You can provide a custom `getUserInfo` function to handle specific provider requirements:
```ts
genericOAuth({
config: [
{
providerId: "custom-provider",
// ... other config options
getUserInfo: async (tokens) => {
// Custom logic to fetch and return user info
const userInfo = await fetchUserInfoFromCustomProvider(tokens);
return {
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
// ... map other fields as needed
};
}
}
]
})
```
### Map User Info Fields
If the user info returned by the provider does not match the expected format, or you need to map additional fields, you can use the `mapProfileToUser`:
```ts
genericOAuth({
config: [
{
providerId: "custom-provider",
// ... other config options
mapProfileToUser: async (profile) => {
return {
firstName: profile.given_name,
// ... map other fields as needed
};
}
}
]
})
```
### Error Handling
The plugin includes built-in error handling for common OAuth issues. Errors are typically redirected to your application's error page with an appropriate error message in the URL parameters. If the callback URL is not provided, the user will be redirected to Better Auth's default error page.
# plugins: Have I Been Pwned
URL: /docs/plugins/have-i-been-pwned
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/have-i-been-pwned.mdx
A plugin to check if a password has been compromised
***
title: Have I Been Pwned
description: A plugin to check if a password has been compromised
-----------------------------------------------------------------
The Have I Been Pwned plugin helps protect user accounts by preventing the use of passwords that have been exposed in known data breaches. It uses the [Have I Been Pwned](https://haveibeenpwned.com/) API to check if a password has been compromised.
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { haveIBeenPwned } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
plugins: [
haveIBeenPwned()
]
})
```
## Usage
When a user attempts to create an account or update their password with a compromised password, they'll receive the following default error:
```json
{
"code": "PASSWORD_COMPROMISED",
"message": "Password is compromised"
}
```
## Config
You can customize the error message:
```ts
haveIBeenPwned({
customPasswordCompromisedMessage: "Please choose a more secure password."
})
```
## Security Notes
* Only the first 5 characters of the password hash are sent to the API
* The full password is never transmitted
* Provides an additional layer of account security
# plugins: JWT
URL: /docs/plugins/jwt
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/jwt.mdx
Authenticate users with JWT tokens in services that can't use the session
***
title: JWT
description: Authenticate users with JWT tokens in services that can't use the session
--------------------------------------------------------------------------------------
The JWT plugin provides endpoints to retrieve a JWT token and a JWKS endpoint to verify the token.
This plugin is not meant as a replacement for the session. It's meant to be used for services that require JWT tokens. If you're looking to use JWT tokens for authentication, check out the [Bearer Plugin](/docs/plugins/bearer).
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
jwt(), // [!code highlight]
] // [!code highlight]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
## Usage
Once you've installed the plugin, you can start using the JWT & JWKS plugin to get the token and the JWKS through their respective endpoints.
## JWT
### Retrieve the token
1. Using your session token
To get the token, call the `/token` endpoint. This will return the following:
```json
{
"token": "ey..."
}
```
Make sure to include the token in the `Authorization` header of your requests if the `bearer` plugin is added in your auth configuration.
```ts
await fetch("/api/auth/token", {
headers: {
"Authorization": `Bearer ${token}`
},
})
```
2. From `set-auth-jwt` header
When you call `getSession` method, a JWT is returned in the `set-auth-jwt` header, which you can use to send to your services directly.
```ts
await authClient.getSession({
fetchOptions: {
onSuccess: (ctx)=>{
const jwt = ctx.response.headers.get("set-auth-jwt")
}
}
})
```
### Verifying the token
The token can be verified in your own service, without the need for an additional verify call or database check.
For this JWKS is used. The public key can be fetched from the `/api/auth/jwks` endpoint.
Since this key is not subject to frequent changes, it can be cached indefinitely.
The key ID (`kid`) that was used to sign a JWT is included in the header of the token.
In case a JWT with a different `kid` is received, it is recommended to fetch the JWKS again.
```json
{
"keys": [
{
"crv": "Ed25519",
"x": "bDHiLTt7u-VIU7rfmcltcFhaHKLVvWFy-_csKZARUEU",
"kty": "OKP",
"kid": "c5c7995d-0037-4553-8aee-b5b620b89b23"
}
]
}
```
### OAuth Provider Mode
If you are making your system oAuth compliant (such as when utilizing the OIDC or MCP plugins), you **MUST** disable the `/token` endpoint (oAuth equivalent `/oauth2/token`) and disable setting the jwt header (oAuth equivalent `/oauth2/userinfo`).
```ts title="auth.ts"
betterAuth({
disabledPaths: [
"/token",
],
plugins: [jwt({
disableSettingJwtHeader: true,
})]
})
```
#### Example using jose with remote JWKS
```ts
import { jwtVerify, createRemoteJWKSet } from 'jose'
async function validateToken(token: string) {
try {
const JWKS = createRemoteJWKSet(
new URL('http://localhost:3000/api/auth/jwks')
)
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'http://localhost:3000', // Should match your JWT issuer, which is the BASE_URL
audience: 'http://localhost:3000', // Should match your JWT audience, which is the BASE_URL by default
})
return payload
} catch (error) {
console.error('Token validation failed:', error)
throw error
}
}
// Usage example
const token = 'your.jwt.token' // this is the token you get from the /api/auth/token endpoint
const payload = await validateToken(token)
```
#### Example with local JWKS
```ts
import { jwtVerify, createLocalJWKSet } from 'jose'
async function validateToken(token: string) {
try {
/**
* This is the JWKS that you get from the /api/auth/
* jwks endpoint
*/
const storedJWKS = {
keys: [{
//...
}]
};
const JWKS = createLocalJWKSet({
keys: storedJWKS.data?.keys!,
})
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'http://localhost:3000', // Should match your JWT issuer, which is the BASE_URL
audience: 'http://localhost:3000', // Should match your JWT audience, which is the BASE_URL by default
})
return payload
} catch (error) {
console.error('Token validation failed:', error)
throw error
}
}
// Usage example
const token = 'your.jwt.token' // this is the token you get from the /api/auth/token endpoint
const payload = await validateToken(token)
```
### Remote JWKS Url
Disables the `/jwks` endpoint and uses this endpoint in any discovery such as OIDC.
Useful if your JWKS are not managed at `/jwks` or if your jwks are signed with a certificate and placed on your CDN.
NOTE: you **MUST** specify which asymmetric algorithm is used for signing.
```ts title="auth.ts"
jwt({
jwks: {
remoteUrl: "https://example.com/.well-known/jwks.json",
keyPairConfig: {
alg: 'ES256',
},
}
})
```
### Custom Signing
This is an advanced feature. Configuration outside of this plugin **MUST** be provided.
Implementers:
* `remoteUrl` must be defined if using the `sign` function. This shall store all active keys, not just the current one.
* If using localized approach, ensure server uses the latest private key when rotated. Depending on deployment, the server may need to be restarted.
* When using remote approach, verify the payload is unchanged after transit. Use integrity validation like CRC32 or SHA256 checks if available.
#### Localized Signing
```ts title="auth.ts"
jwt({
jwks: {
remoteUrl: "https://example.com/.well-known/jwks.json",
keyPairConfig: {
alg: 'EdDSA',
},
},
jwt: {
sign: async (jwtPayload: JWTPayload) => {
// this is pseudocode
return await new SignJWT(jwtPayload)
.setProtectedHeader({
alg: "EdDSA",
kid: process.env.currentKid,
typ: "JWT",
})
.sign(process.env.clientPrivateKey);
},
},
})
```
#### Remote Signing
Useful if you are using a remote Key Management Service such as [Google KMS](https://cloud.google.com/kms/docs/encrypt-decrypt-rsa#kms-encrypt-asymmetric-nodejs), [Amazon KMS](https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html), or [Azure Key Vault](https://learn.microsoft.com/en-us/rest/api/keyvault/keys/sign/sign?view=rest-keyvault-keys-7.4\&tabs=HTTP).
```ts title="auth.ts"
jwt({
jwks: {
remoteUrl: "https://example.com/.well-known/jwks.json",
keyPairConfig: {
alg: 'ES256',
},
},
jwt: {
sign: async (jwtPayload: JWTPayload) => {
// this is pseudocode
const headers = JSON.stringify({ kid: '123', alg: 'ES256', typ: 'JWT' })
const payload = JSON.stringify(jwtPayload)
const encodedHeaders = Buffer.from(headers).toString('base64url')
const encodedPayload = Buffer.from(payload).toString('base64url')
const hash = createHash('sha256')
const data = `${encodedHeaders}.${encodedPayload}`
hash.update(Buffer.from(data))
const digest = hash.digest()
const sig = await remoteSign(digest)
// integrityCheck(sig)
const jwt = `${data}.${sig}`
// verifyJwt(jwt)
return jwt
},
},
})
```
## Schema
The JWT plugin adds the following tables to the database:
### JWKS
Table Name: `jwks`
You can customize the table name and fields for the `jwks` table. See the [Database concept documentation](/docs/concepts/database#custom-table-names) for more information on how to customize plugin schema.
## Options
### Algorithm of the Key Pair
The algorithm used for the generation of the key pair. The default is **EdDSA** with the **Ed25519** curve. Below are the available options:
```ts title="auth.ts"
jwt({
jwks: {
keyPairConfig: {
alg: "EdDSA",
crv: "Ed25519"
}
}
})
```
#### EdDSA
* **Default Curve**: `Ed25519`
* **Optional Property**: `crv`
* Available options: `Ed25519`, `Ed448`
* Default: `Ed25519`
#### ES256
* No additional properties
#### RSA256
* **Optional Property**: `modulusLength`
* Expects a number
* Default: `2048`
#### PS256
* **Optional Property**: `modulusLength`
* Expects a number
* Default: `2048`
#### ECDH-ES
* **Optional Property**: `crv`
* Available options: `P-256`, `P-384`, `P-521`
* Default: `P-256`
#### ES512
* No additional properties
### Disable private key encryption
By default, the private key is encrypted using AES256 GCM. You can disable this by setting the `disablePrivateKeyEncryption` option to `true`.
For security reasons, it's recommended to keep the private key encrypted.
```ts title="auth.ts"
jwt({
jwks: {
disablePrivateKeyEncryption: true
}
})
```
### Modify JWT payload
By default the entire user object is added to the JWT payload. You can modify the payload by providing a function to the `definePayload` option.
```ts title="auth.ts"
jwt({
jwt: {
definePayload: ({user}) => {
return {
id: user.id,
email: user.email,
role: user.role
}
}
}
})
```
### Modify Issuer, Audience, Subject or Expiration time
If none is given, the `BASE_URL` is used as the issuer and the audience is set to the `BASE_URL`. The expiration time is set to 15 minutes.
```ts title="auth.ts"
jwt({
jwt: {
issuer: "https://example.com",
audience: "https://example.com",
expirationTime: "1h",
getSubject: (session) => {
// by default the subject is the user id
return session.user.email
}
}
})
```
# plugins: Last Login Method
URL: /docs/plugins/last-login-method
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/last-login-method.mdx
Track and display the last authentication method used by users
***
title: Last Login Method
description: Track and display the last authentication method used by users
---------------------------------------------------------------------------
The last login method plugin tracks the most recent authentication method used by users (email, OAuth providers, etc.). This enables you to display helpful indicators on login pages, such as "Last signed in with Google" or prioritize certain login methods based on user preferences.
## Installation
### Add the plugin to your auth config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
lastLoginMethod() // [!code highlight]
]
})
```
### Add the client plugin to your auth client
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { lastLoginMethodClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
lastLoginMethodClient() // [!code highlight]
]
})
```
## Usage
Once installed, the plugin automatically tracks the last authentication method used by users. You can then retrieve and display this information in your application.
### Getting the Last Used Method
The client plugin provides several methods to work with the last login method:
```ts title="app.tsx"
import { authClient } from "@/lib/auth-client"
// Get the last used login method
const lastMethod = authClient.getLastUsedLoginMethod()
console.log(lastMethod) // "google", "email", "github", etc.
// Check if a specific method was last used
const wasGoogle = authClient.isLastUsedLoginMethod("google")
// Clear the stored method
authClient.clearLastUsedLoginMethod()
```
### UI Integration Example
Here's how to use the plugin to enhance your login page:
```tsx title="sign-in.tsx"
import { authClient } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
export function SignInPage() {
const lastMethod = authClient.getLastUsedLoginMethod()
return (
Sign In
{/* Email sign in */}
{/* OAuth providers */}
)
}
```
## Database Persistence
By default, the last login method is stored only in cookies. For more persistent tracking and analytics, you can enable database storage.
### Enable database storage
Set `storeInDatabase` to `true` in your plugin configuration:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
lastLoginMethod({
storeInDatabase: true // [!code highlight]
})
]
})
```
### Run database migration
The plugin will automatically add a `lastLoginMethod` field to your user table. Run the migration to apply the changes:
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
### Access database field
When database storage is enabled, the `lastLoginMethod` field becomes available in user objects:
```ts title="user-profile.tsx"
import { auth } from "@/lib/auth"
// Server-side access
const session = await auth.api.getSession({ headers })
console.log(session?.user.lastLoginMethod) // "google", "email", etc.
// Client-side access via session
const { data: session } = authClient.useSession()
console.log(session?.user.lastLoginMethod)
```
### Database Schema
When `storeInDatabase` is enabled, the plugin adds the following field to the `user` table:
Table: `user`
### Custom Schema Configuration
You can customize the database field name:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
lastLoginMethod({
storeInDatabase: true,
schema: {
user: {
lastLoginMethod: "last_auth_method" // Custom field name
}
}
})
]
})
```
## Configuration Options
The last login method plugin accepts the following options:
### Server Options
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
lastLoginMethod({
// Cookie configuration
cookieName: "better-auth.last_used_login_method", // Default: "better-auth.last_used_login_method"
maxAge: 60 * 60 * 24 * 30, // Default: 30 days in seconds
// Database persistence
storeInDatabase: false, // Default: false
// Custom method resolution
customResolveMethod: (ctx) => {
// Custom logic to determine the login method
if (ctx.path === "/oauth/callback/custom-provider") {
return "custom-provider"
}
// Return null to use default resolution
return null
},
// Schema customization (when storeInDatabase is true)
schema: {
user: {
lastLoginMethod: "custom_field_name"
}
}
})
]
})
```
**cookieName**: `string`
* The name of the cookie used to store the last login method
* Default: `"better-auth.last_used_login_method"`
* **Note**: This cookie is `httpOnly: false` to allow client-side JavaScript access for UI features
**maxAge**: `number`
* Cookie expiration time in seconds
* Default: `2592000` (30 days)
**storeInDatabase**: `boolean`
* Whether to store the last login method in the database
* Default: `false`
* When enabled, adds a `lastLoginMethod` field to the user table
**customResolveMethod**: `(ctx: GenericEndpointContext) => string | null`
* Custom function to determine the login method from the request context
* Return `null` to use the default resolution logic
* Useful for custom OAuth providers or authentication flows
**schema**: `object`
* Customize database field names when `storeInDatabase` is enabled
* Allows mapping the `lastLoginMethod` field to a custom column name
### Client Options
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { lastLoginMethodClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
lastLoginMethodClient({
cookieName: "better-auth.last_used_login_method" // Default: "better-auth.last_used_login_method"
})
]
})
```
**cookieName**: `string`
* The name of the cookie to read the last login method from
* Must match the server-side `cookieName` configuration
* Default: `"better-auth.last_used_login_method"`
### Default Method Resolution
By default, the plugin tracks these authentication methods:
* **Email authentication**: `"email"`
* **OAuth providers**: Provider ID (e.g., `"google"`, `"github"`, `"discord"`)
* **OAuth2 callbacks**: Provider ID from URL path
* **Sign up methods**: Tracked the same as sign in methods
The plugin automatically detects the method from these endpoints:
* `/callback/:id` - OAuth callback with provider ID
* `/oauth2/callback/:id` - OAuth2 callback with provider ID
* `/sign-in/email` - Email sign in
* `/sign-up/email` - Email sign up
## Cross-Domain Support
The plugin automatically inherits cookie settings from Better Auth's centralized cookie system. This solves the problem where the last login method wouldn't persist across:
* **Cross-subdomain setups**: `auth.example.com` → `app.example.com`
* **Cross-origin setups**: `api.company.com` → `app.different.com`
When you enable `crossSubDomainCookies` or `crossOriginCookies` in your Better Auth config, the plugin will automatically use the same domain, secure, and sameSite settings as your session cookies, ensuring consistent behavior across your application.
## Advanced Examples
### Custom Provider Tracking
If you have custom OAuth providers or authentication methods, you can use the `customResolveMethod` option:
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
lastLoginMethod({
customResolveMethod: (ctx) => {
// Track custom SAML provider
if (ctx.path === "/saml/callback") {
return "saml"
}
// Track magic link authentication
if (ctx.path === "/verify-magic-link") {
return "magic-link"
}
// Track phone authentication
if (ctx.path === "/sign-in/phone") {
return "phone"
}
// Return null to use default logic
return null
}
})
]
})
```
# plugins: Magic link
URL: /docs/plugins/magic-link
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/magic-link.mdx
Magic link plugin
***
title: Magic link
description: Magic link plugin
------------------------------
Magic link or email link is a way to authenticate users without a password. When a user enters their email, a link is sent to their email. When the user clicks on the link, they are authenticated.
## Installation
### Add the server Plugin
Add the magic link plugin to your server:
```ts title="server.ts"
import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
magicLink({
sendMagicLink: async ({ email, token, url }, request) => {
// send email to user
}
})
]
})
```
### Add the client Plugin
Add the magic link plugin to your client:
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { magicLinkClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
magicLinkClient()
]
});
```
## Usage
### Sign In with Magic Link
To sign in with a magic link, you need to call `signIn.magicLink` with the user's email address. The `sendMagicLink` function is called to send the magic link to the user's email.
### Client Side
```ts
const { data, error } = await authClient.signIn.magicLink({
email: user@email.com,
name: my-name, // required
callbackURL: /dashboard, // required
newUserCallbackURL: /welcome, // required
errorCallbackURL: /error, // required
});
```
### Server Side
```ts
const data = await auth.api.signInMagicLink({
body: {
email: user@email.com,
name: my-name, // required
callbackURL: /dashboard, // required
newUserCallbackURL: /welcome, // required
errorCallbackURL: /error, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type signInMagicLink = {
/**
* Email address to send the magic link.
*/
email: string = "user@email.com"
/**
* User display name. Only used if the user is registering for the first time.
*/
name?: string = "my-name"
/**
* URL to redirect after magic link verification.
*/
callbackURL?: string = "/dashboard"
/**
* URL to redirect after new user signup
*/
newUserCallbackURL?: string = "/welcome"
/**
* URL to redirect if an error happen on verification
* If only callbackURL is provided but without an `errorCallbackURL` then they will be
* redirected to the callbackURL with an `error` query parameter.
*/
errorCallbackURL?: string = "/error"
}
```
If the user has not signed up, unless `disableSignUp` is set to `true`, the user will be signed up automatically.
### Verify Magic Link
When you send the URL generated by the `sendMagicLink` function to a user, clicking the link will authenticate them and redirect them to the `callbackURL` specified in the `signIn.magicLink` function. If an error occurs, the user will be redirected to the `callbackURL` with an error query parameter.
If no `callbackURL` is provided, the user will be redirected to the root URL.
If you want to handle the verification manually, (e.g, if you send the user a different URL), you can use the `verify` function.
### Client Side
```ts
const { data, error } = await authClient.magicLink.verify({
token: 123456,
callbackURL: /dashboard, // required
});
```
### Server Side
```ts
const data = await auth.api.magicLinkVerify({
query: {
token: 123456,
callbackURL: /dashboard, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type magicLinkVerify = {
/**
* Verification token.
*/
token: string = "123456"
/**
* URL to redirect after magic link verification, if not provided will return the session.
*/
callbackURL?: string = "/dashboard"
}
```
## Configuration Options
**sendMagicLink**: The `sendMagicLink` function is called when a user requests a magic link. It takes an object with the following properties:
* `email`: The email address of the user.
* `url`: The URL to be sent to the user. This URL contains the token.
* `token`: The token if you want to send the token with custom URL.
and a `request` object as the second parameter.
**expiresIn**: specifies the time in seconds after which the magic link will expire. The default value is `300` seconds (5 minutes).
**disableSignUp**: If set to `true`, the user will not be able to sign up using the magic link. The default value is `false`.
**generateToken**: The `generateToken` function is called to generate a token which is used to uniquely identify the user. The default value is a random string. There is one parameter:
* `email`: The email address of the user.
When using `generateToken`, ensure that the returned string is hard to guess
because it is used to verify who someone actually is in a confidential way. By
default, we return a long and cryptographically secure string.
**storeToken**: The `storeToken` function is called to store the magic link token in the database. The default value is `"plain"`.
The `storeToken` function can be one of the following:
* `"plain"`: The token is stored in plain text.
* `"hashed"`: The token is hashed using the default hasher.
* `{ type: "custom-hasher", hash: (token: string) => Promise }`: The token is hashed using a custom hasher.
# plugins: MCP
URL: /docs/plugins/mcp
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/mcp.mdx
MCP provider plugin for Better Auth
***
title: MCP
description: MCP provider plugin for Better Auth
------------------------------------------------
`OAuth` `MCP`
The **MCP** plugin lets your app act as an OAuth provider for MCP clients. It handles authentication and makes it easy to issue and manage access tokens for MCP applications.
## Installation
### Add the Plugin
Add the MCP plugin to your auth configuration and specify the login page path.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { mcp } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
mcp({
loginPage: "/sign-in" // path to your login page
})
]
});
```
This doesn't have a client plugin, so you don't need to make any changes to your authClient.
### Generate Schema
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details.
## Usage
### OAuth Discovery Metadata
Better Auth already handles the `/api/auth/.well-known/oauth-authorization-server` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-authorization-server` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:
```ts title=".well-known/oauth-authorization-server/route.ts"
import { oAuthDiscoveryMetadata } from "better-auth/plugins";
import { auth } from "../../../lib/auth";
export const GET = oAuthDiscoveryMetadata(auth);
```
### OAuth Protected Resource Metadata
Better Auth already handles the `/api/auth/.well-known/oauth-protected-resource` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-protected-resource` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:
```ts title="/.well-known/oauth-protected-resource/route.ts"
import { oAuthProtectedResourceMetadata } from "better-auth/plugins";
import { auth } from "@/lib/auth";
export const GET = oAuthProtectedResourceMetadata(auth);
```
### MCP Session Handling
You can use the helper function `withMcpAuth` to get the session and handle unauthenticated calls automatically.
```ts title="api/[transport]/route.ts"
import { auth } from "@/lib/auth";
import { createMcpHandler } from "@vercel/mcp-adapter";
import { withMcpAuth } from "better-auth/plugins";
import { z } from "zod";
const handler = withMcpAuth(auth, (req, session) => {
// session contains the access token record with scopes and user ID
return createMcpHandler(
(server) => {
server.tool(
"echo",
"Echo a message",
{ message: z.string() },
async ({ message }) => {
return {
content: [{ type: "text", text: `Tool echo: ${message}` }],
};
},
);
},
{
capabilities: {
tools: {
echo: {
description: "Echo a message",
},
},
},
},
{
redisUrl: process.env.REDIS_URL,
basePath: "/api",
verboseLogs: true,
maxDuration: 60,
},
)(req);
});
export { handler as GET, handler as POST, handler as DELETE };
```
You can also use `auth.api.getMcpSession` to get the session using the access token sent from the MCP client:
```ts title="api/[transport]/route.ts"
import { auth } from "@/lib/auth";
import { createMcpHandler } from "@vercel/mcp-adapter";
import { z } from "zod";
const handler = async (req: Request) => {
// session contains the access token record with scopes and user ID
const session = await auth.api.getMcpSession({
headers: req.headers
})
if(!session){
//this is important and you must return 401
return new Response(null, {
status: 401
})
}
return createMcpHandler(
(server) => {
server.tool(
"echo",
"Echo a message",
{ message: z.string() },
async ({ message }) => {
return {
content: [{ type: "text", text: `Tool echo: ${message}` }],
};
},
);
},
{
capabilities: {
tools: {
echo: {
description: "Echo a message",
},
},
},
},
{
redisUrl: process.env.REDIS_URL,
basePath: "/api",
verboseLogs: true,
maxDuration: 60,
},
)(req);
}
export { handler as GET, handler as POST, handler as DELETE };
```
## Configuration
The MCP plugin accepts the following configuration options:
### OIDC Configuration
The plugin supports additional OIDC configuration options through the `oidcConfig` parameter:
## Schema
The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details.
# plugins: Multi Session
URL: /docs/plugins/multi-session
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/multi-session.mdx
Learn how to use multi-session plugin in Better Auth.
***
title: Multi Session
description: Learn how to use multi-session plugin in Better Auth.
------------------------------------------------------------------
The multi-session plugin allows users to maintain multiple active sessions across different accounts in the same browser. This plugin is useful for applications that require users to switch between multiple accounts without logging out.
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { multiSession } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
multiSession(), // [!code highlight]
] // [!code highlight]
})
```
### Add the client Plugin
Add the client plugin and Specify where the user should be redirected if they need to verify 2nd factor
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { multiSessionClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
multiSessionClient()
]
})
```
## Usage
Whenever a user logs in, the plugin will add additional cookie to the browser. This cookie will be used to maintain multiple sessions across different accounts.
### List all device sessions
To list all active sessions for the current user, you can call the `listDeviceSessions` method.
### Client Side
```ts
const { data, error } = await authClient.multiSession.listDeviceSessions({});
```
### Server Side
```ts
const data = await auth.api.listDeviceSessions({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listDeviceSessions = {
}
```
### Set active session
To set the active session, you can call the `setActive` method.
### Client Side
```ts
const { data, error } = await authClient.multiSession.setActive({
sessionToken: some-session-token,
});
```
### Server Side
```ts
const data = await auth.api.setActiveSession({
body: {
sessionToken: some-session-token,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type setActiveSession = {
/**
* The session token to set as active.
*/
sessionToken: string = "some-session-token"
}
```
### Revoke a session
To revoke a session, you can call the `revoke` method.
### Client Side
```ts
const { data, error } = await authClient.multiSession.revoke({
sessionToken: some-session-token,
});
```
### Server Side
```ts
const data = await auth.api.revokeDeviceSession({
body: {
sessionToken: some-session-token,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type revokeDeviceSession = {
/**
* The session token to revoke.
*/
sessionToken: string = "some-session-token"
}
```
### Signout and Revoke all sessions
When a user logs out, the plugin will revoke all active sessions for the user. You can do this by calling the existing `signOut` method, which handles revoking all sessions automatically.
### Max Sessions
You can specify the maximum number of sessions a user can have by passing the `maximumSessions` option to the plugin. By default, the plugin allows 5 sessions per device.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
multiSession({
maximumSessions: 3
})
]
})
```
# plugins: OAuth Proxy
URL: /docs/plugins/oauth-proxy
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/oauth-proxy.mdx
OAuth Proxy plugin for Better Auth
***
title: OAuth Proxy
description: OAuth Proxy plugin for Better Auth
-----------------------------------------------
A proxy plugin, that allows you to proxy OAuth requests. Useful for development and preview deployments where the redirect URL can't be known in advance to add to the OAuth provider.
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { oAuthProxy } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
oAuthProxy({ // [!code highlight]
productionURL: "https://my-main-app.com", // Optional - if the URL isn't inferred correctly // [!code highlight]
currentURL: "http://localhost:3000", // Optional - if the URL isn't inferred correctly // [!code highlight]
}), // [!code highlight]
] // [!code highlight]
})
```
### Add redirect URL to your OAuth provider
For the proxy server to work properly, you’ll need to pass the redirect URL of your main production app registered with the OAuth provider in your social provider config. This needs to be done for each social provider you want to proxy requests for.
```ts
export const auth = betterAuth({
plugins: [
oAuthProxy(),
],
socialProviders: {
github: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectURI: "https://my-main-app.com/api/auth/callback/github" // [!code highlight]
}
}
})
```
## How it works
The plugin adds an endpoint to your server that proxies OAuth requests. When you initiate a social sign-in, it sets the redirect URL to this proxy endpoint. After the OAuth provider redirects back to your server, the plugin then forwards the user to the original callback URL.
```ts
await authClient.signIn.social({
provider: "github",
callbackURL: "/dashboard" // the plugin will override this to something like "http://localhost:3000/api/auth/oauth-proxy?callbackURL=/dashboard"
})
```
When the OAuth provider returns the user to your server, the plugin automatically redirects them to the intended callback URL.
To share cookies between the proxy server and your main server it uses URL query parameters to pass the cookies encrypted in the URL. This is secure as the cookies are encrypted and can only be decrypted by the server.
## Options
**currentURL**: The application's current URL is automatically determined by the plugin. It first checks for the request URL if invoked by a client, then it checks the base URL from popular hosting providers, and finally falls back to the `baseURL` in your auth config. If the URL isn’t inferred correctly, you can specify it manually here.
**productionURL**: If this value matches the `baseURL` in your auth config, requests will not be proxied. Defaults to the `BETTER_AUTH_URL` environment variable.
# plugins: OIDC Provider
URL: /docs/plugins/oidc-provider
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/oidc-provider.mdx
Open ID Connect plugin for Better Auth that allows you to have your own OIDC provider.
***
title: OIDC Provider
description: Open ID Connect plugin for Better Auth that allows you to have your own OIDC provider.
---------------------------------------------------------------------------------------------------
The **OIDC Provider Plugin** enables you to build and manage your own OpenID Connect (OIDC) provider, granting full control over user authentication without relying on third-party services like Okta or Azure AD. It also allows other services to authenticate users through your OIDC provider.
**Key Features**:
* **Client Registration**: Register clients to authenticate with your OIDC provider.
* **Dynamic Client Registration**: Allow clients to register dynamically.
* **Trusted Clients**: Configure hard-coded trusted clients with optional consent bypass.
* **Authorization Code Flow**: Support the Authorization Code Flow.
* **Public Clients**: Support public clients for SPA, mobile apps, CLI tools, etc.
* **JWKS Endpoint**: Publish a JWKS endpoint to allow clients to verify tokens. (Not fully implemented)
* **Refresh Tokens**: Issue refresh tokens and handle access token renewal using the `refresh_token` grant.
* **OAuth Consent**: Implement OAuth consent screens for user authorization, with an option to bypass consent for trusted applications.
* **UserInfo Endpoint**: Provide a UserInfo endpoint for clients to retrieve user details.
This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on [GitHub](https://github.com/better-auth/better-auth).
## Installation
### Mount the Plugin
Add the OIDC plugin to your auth config. See [OIDC Configuration](#oidc-configuration) on how to configure the plugin.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
const auth = betterAuth({
plugins: [oidcProvider({
loginPage: "/sign-in", // path to the login page
// ...other options
})]
})
```
### Migrate the Database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the Client Plugin
Add the OIDC client plugin to your auth client config.
```ts
import { createAuthClient } from "better-auth/client";
import { oidcClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [oidcClient({
// Your OIDC configuration
})]
})
```
## Usage
Once installed, you can utilize the OIDC Provider to manage authentication flows within your application.
### Register a New Client
To register a new OIDC client, use the `oauth2.register` method.
#### Simple Example
```ts
const application = await client.oauth2.register({
client_name: "My Client",
redirect_uris: ["https://client.example.com/callback"],
});
```
#### Full Method
### Client Side
```ts
const { data, error } = await authClient.oauth2.register({
redirect_uris, // client.example.com/callback"]
token_endpoint_auth_method: client_secret_basic, // required
grant_types, // required
response_types, // required
client_name: My App, // required
client_uri: https://client.example.com, // required
logo_uri: https://client.example.com/logo.png, // required
scope: profile email, // required
contacts, // required
tos_uri: https://client.example.com/tos, // required
policy_uri: https://client.example.com/policy, // required
jwks_uri: https://client.example.com/jwks, // required
jwks, // required
});
```
### Server Side
```ts
const data = await auth.api.registerOAuthApplication({
body: {
redirect_uris, // client.example.com/callback"]
token_endpoint_auth_method: client_secret_basic, // required
grant_types, // required
response_types, // required
client_name: My App, // required
client_uri: https://client.example.com, // required
logo_uri: https://client.example.com/logo.png, // required
scope: profile email, // required
contacts, // required
tos_uri: https://client.example.com/tos, // required
policy_uri: https://client.example.com/policy, // required
jwks_uri: https://client.example.com/jwks, // required
jwks, // required
}
});
```
### Type Definition
```ts
type registerOAuthApplication = {
/**
* A list of redirect URIs.
*/
redirect_uris: string[] = ["https://client.example.com/callback"]
/**
* The authentication method for the token endpoint.
*/
token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" = "client_secret_basic"
/**
* The grant types supported by the application.
*/
grant_types?: ("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[] = ["authorization_code"]
/**
* The response types supported by the application.
*/
response_types?: ("code" | "token")[] = ["code"]
/**
* The name of the application.
*/
client_name?: string = "My App"
/**
* The URI of the application.
*/
client_uri?: string = "https://client.example.com"
/**
* The URI of the application logo.
*/
logo_uri?: string = "https://client.example.com/logo.png"
/**
* The scopes supported by the application. Separated by spaces.
*/
scope?: string = "profile email"
/**
* The contact information for the application.
*/
contacts?: string[] = ["admin@example.com"]
/**
* The URI of the application terms of service.
*/
tos_uri?: string = "https://client.example.com/tos"
/**
* The URI of the application privacy policy.
*/
policy_uri?: string = "https://client.example.com/policy"
/**
* The URI of the application JWKS.
*/
jwks_uri?: string = "https://client.example.com/jwks"
/**
* The JWKS of the application.
*/
jwks?: Record = {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."
}
```
This endpoint supports [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration.
Once the application is created, you will receive a `client_id` and `client_secret` that you can display to the user.
### Trusted Clients
For first-party applications and internal services, you can configure trusted clients directly in your OIDC provider configuration. Trusted clients bypass database lookups for better performance and can optionally skip consent screens for improved user experience.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
const auth = betterAuth({
plugins: [
oidcProvider({
loginPage: "/sign-in",
trustedClients: [
{
clientId: "internal-dashboard",
clientSecret: "secure-secret-here",
name: "Internal Dashboard",
type: "web",
redirectURLs: ["https://dashboard.company.com/auth/callback"],
disabled: false,
skipConsent: true, // Skip consent for this trusted client
metadata: { internal: true }
},
{
clientId: "mobile-app",
clientSecret: "mobile-secret",
name: "Company Mobile App",
type: "native",
redirectURLs: ["com.company.app://auth"],
disabled: false,
skipConsent: false, // Still require consent if needed
metadata: {}
}
]
})]
})
```
### UserInfo Endpoint
The OIDC Provider includes a UserInfo endpoint that allows clients to retrieve information about the authenticated user. This endpoint is available at `/oauth2/userinfo` and requires a valid access token.
```ts title="client-app.ts"
// Example of how a client would use the UserInfo endpoint
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
headers: {
'Authorization': 'Bearer ACCESS_TOKEN'
}
});
const userInfo = await response.json();
// userInfo contains user details based on the scopes granted
```
The UserInfo endpoint returns different claims based on the scopes that were granted during authorization:
* With `openid` scope: Returns the user's ID (`sub` claim)
* With `profile` scope: Returns name, picture, given\_name, family\_name
* With `email` scope: Returns email and email\_verified
The `getAdditionalUserInfoClaim` function receives the user object, requested scopes array, and the client, allowing you to conditionally include claims based on the scopes granted during authorization. These additional claims will be included in both the UserInfo endpoint response and the ID token.
### Consent Screen
When a user is redirected to the OIDC provider for authentication, they may be prompted to authorize the application to access their data. This is known as the consent screen. By default, Better Auth will display a sample consent screen. You can customize the consent screen by providing a `consentPage` option during initialization.
**Note**: Trusted clients with `skipConsent: true` will bypass the consent screen entirely, providing a seamless experience for first-party applications.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [oidcProvider({
consentPage: "/path/to/consent/page"
})]
})
```
The plugin will redirect the user to the specified path with `consent_code`, `client_id` and `scope` query parameters. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization.
The consent endpoint supports two methods for passing the consent code:
**Method 1: URL Parameter**
```ts title="consent-page.ts"
// Get the consent code from the URL
const params = new URLSearchParams(window.location.search);
// Submit consent with the code in the request body
const consentCode = params.get('consent_code');
if (!consentCode) {
throw new Error('Consent code not found in URL parameters');
}
const res = await client.oauth2.consent({
accept: true, // or false to deny
consent_code: consentCode,
});
```
**Method 2: Cookie-Based**
```ts title="consent-page.ts"
// The consent code is automatically stored in a signed cookie
// Just submit the consent decision
const res = await client.oauth2.consent({
accept: true, // or false to deny
// consent_code not needed when using cookie-based flow
});
```
Both methods are fully supported. The URL parameter method works well with mobile apps and third-party contexts, while the cookie-based method provides a simpler implementation for web applications.
### Handling Login
When a user is redirected to the OIDC provider for authentication, if they are not already logged in, they will be redirected to the login page. You can customize the login page by providing a `loginPage` option during initialization.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [oidcProvider({
loginPage: "/sign-in"
})]
})
```
You don't need to handle anything from your side; when a new session is created, the plugin will handle continuing the authorization flow.
## Configuration
### OIDC Metadata
Customize the OIDC metadata by providing a configuration object during initialization.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [oidcProvider({
metadata: {
issuer: "https://your-domain.com",
authorization_endpoint: "/custom/oauth2/authorize",
token_endpoint: "/custom/oauth2/token",
// ...other custom metadata
}
})]
})
```
### JWKS Endpoint
The OIDC Provider plugin can integrate with the JWT plugin to provide asymmetric key signing for ID tokens verifiable at a JWKS endpoint.
To make your plugin OIDC compliant, you **MUST** disable the `/token` endpoint, the OAuth equivalent is located at `/oauth2/token` instead.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({
disabledPaths: [
"/token",
],
plugins: [
jwt(), // Make sure to add the JWT plugin
oidcProvider({
useJWTPlugin: true, // Enable JWT plugin integration
loginPage: "/sign-in",
// ... other options
})
]
})
```
When `useJWTPlugin: false` (default), ID tokens are signed with the application secret.
### Dynamic Client Registration
If you want to allow clients to register dynamically, you can enable this feature by setting the `allowDynamicClientRegistration` option to `true`.
```ts title="auth.ts"
const auth = betterAuth({
plugins: [oidcProvider({
allowDynamicClientRegistration: true,
})]
})
```
This will allow clients to register using the `/register` endpoint to be publicly available.
## Schema
The OIDC Provider plugin adds the following tables to the database:
### OAuth Application
Table Name: `oauthApplication`
### OAuth Access Token
Table Name: `oauthAccessToken`
### OAuth Consent
Table Name: `oauthConsent`
## Options
**allowDynamicClientRegistration**: `boolean` - Enable or disable dynamic client registration.
**metadata**: `OIDCMetadata` - Customize the OIDC provider metadata.
**loginPage**: `string` - Path to the custom login page.
**consentPage**: `string` - Path to the custom consent page.
**trustedClients**: `(Client & { skipConsent?: boolean })[]` - Array of trusted clients that are configured directly in the provider options. These clients bypass database lookups and can optionally skip consent screens.
**getAdditionalUserInfoClaim**: `(user: User, scopes: string[], client: Client) => Record` - Function to get additional user info claims.
**useJWTPlugin**: `boolean` - When `true`, ID tokens are signed using the JWT plugin's asymmetric keys. When `false` (default), ID tokens are signed with HMAC-SHA256 using the application secret.
**schema**: `AuthPluginSchema` - Customize the OIDC provider schema.
# plugins: One Tap
URL: /docs/plugins/one-tap
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/one-tap.mdx
One Tap plugin for Better Auth
***
title: One Tap
description: One Tap plugin for Better Auth
-------------------------------------------
The One Tap plugin allows users to log in with a single tap using Google's One Tap API. The plugin
provides a simple way to integrate One Tap into your application, handling the client-side and server-side logic for you.
## Installation
### Add the Server Plugin
Add the One Tap plugin to your auth configuration:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oneTap } from "better-auth/plugins"; // [!code highlight]
export const auth = betterAuth({
plugins: [ // [!code highlight]
oneTap(), // Add the One Tap server plugin // [!code highlight]
] // [!code highlight]
});
```
### Add the Client Plugin
Add the client plugin and specify where the user should be redirected after sign-in or if additional verification (like 2FA) is needed.
```ts
import { createAuthClient } from "better-auth/client";
import { oneTapClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
oneTapClient({
clientId: "YOUR_CLIENT_ID",
// Optional client configuration:
autoSelect: false,
cancelOnTapOutside: true,
context: "signin",
additionalOptions: {
// Any extra options for the Google initialize method
},
// Configure prompt behavior and exponential backoff:
promptOptions: {
baseDelay: 1000, // Base delay in ms (default: 1000)
maxAttempts: 5 // Maximum number of attempts before triggering onPromptNotification (default: 5)
}
})
]
});
```
### Usage
To display the One Tap popup, simply call the oneTap method on your auth client:
```ts
await authClient.oneTap();
```
### Customizing Redirect Behavior
By default, after a successful login the plugin will hard redirect the user to `/`. You can customize this behavior as follows:
#### Avoiding a Hard Redirect
Pass fetchOptions with an onSuccess callback to handle the login response without a page reload:
```ts
await authClient.oneTap({
fetchOptions: {
onSuccess: () => {
// For example, use a router to navigate without a full reload:
router.push("/dashboard");
}
}
});
```
#### Specifying a Custom Callback URL
To perform a hard redirect to a different page after login, use the callbackURL option:
```ts
await authClient.oneTap({
callbackURL: "/dashboard"
});
```
#### Handling Prompt Dismissals with Exponential Backoff
If the user dismisses or skips the prompt, the plugin will retry showing the One Tap prompt using exponential backoff based on your configured promptOptions.
If the maximum number of attempts is reached without a successful sign-in, you can use the onPromptNotification callback to be notified—allowing you to render an alternative UI (e.g., a traditional Google Sign-In button) so users can restart the process manually:
```ts
await authClient.oneTap({
onPromptNotification: (notification) => {
console.warn("Prompt was dismissed or skipped. Consider displaying an alternative sign-in option.", notification);
// Render your alternative UI here
}
});
```
### Client Options
* **clientId**: The client ID for your Google One Tap API.
* **autoSelect**: Automatically select the account if the user is already signed in. Default is false.
* **context**: The context in which the One Tap API should be used (e.g., "signin"). Default is "signin".
* **cancelOnTapOutside**: Cancel the One Tap popup when the user taps outside it. Default is true.
* additionalOptions: Extra options to pass to Google's initialize method as per the [Google Identity Services docs](https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt).
* **promptOptions**: Configuration for the prompt behavior and exponential backoff:
* **baseDelay**: Base delay in milliseconds for retries. Default is 1000.
* **maxAttempts**: Maximum number of prompt attempts before invoking the onPromptNotification callback. Default is 5.
### Server Options
* **disableSignUp**: Disable the sign-up option, allowing only existing users to sign in. Default is `false`.
* **ClientId**: Optionally, pass a client ID here if it is not provided in your social provider configuration.
# plugins: One-Time Token Plugin
URL: /docs/plugins/one-time-token
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/one-time-token.mdx
Generate and verify single-use token
***
title: One-Time Token Plugin
description: Generate and verify single-use token
-------------------------------------------------
The One-Time Token (OTT) plugin provides functionality to generate and verify secure, single-use session tokens. These are commonly used for across domains authentication.
## Installation
### Add the plugin to your auth config
To use the One-Time Token plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oneTimeToken } from "better-auth/plugins/one-time-token";
export const auth = betterAuth({
plugins: [
oneTimeToken()
]
// ... other auth config
});
```
### Add the client plugin
Next, include the one-time-token client plugin in your authentication client instance.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { oneTimeTokenClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
oneTimeTokenClient()
]
})
```
## Usage
### 1. Generate a Token
Generate a token using `auth.api.generateOneTimeToken` or `authClient.oneTimeToken.generate`
### Client Side
```ts
const { data, error } = await authClient.oneTimeToken.generate({});
```
### Server Side
```ts
const data = await auth.api.generateOneTimeToken({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type generateOneTimeToken = {
}
```
This will return a `token` that is attached to the current session which can be used to verify the one-time token. By default, the token will expire in 3 minutes.
### 2. Verify the Token
When the user clicks the link or submits the token, use the `auth.api.verifyOneTimeToken` or `authClient.oneTimeToken.verify` method in another API route to validate it.
### Client Side
```ts
const { data, error } = await authClient.oneTimeToken.verify({
token: some-token,
});
```
### Server Side
```ts
const data = await auth.api.verifyOneTimeToken({
body: {
token: some-token,
}
});
```
### Type Definition
```ts
type verifyOneTimeToken = {
/**
* The token to verify.
*/
token: string = "some-token"
}
```
This will return the session that was attached to the token.
## Options
These options can be configured when adding the `oneTimeToken` plugin:
* **`disableClientRequest`** (boolean): Optional. If `true`, the token will only be generated on the server side. Default: `false`.
* **`expiresIn`** (number): Optional. The duration for which the token is valid in minutes. Default: `3`.
```ts
oneTimeToken({
expiresIn: 10 // 10 minutes
})
```
* **`generateToken`**: A custom token generator function that takes `session` object and a `ctx` as paramters.
* **`storeToken`**: Optional. This option allows you to configure how the token is stored in your database.
* **`plain`**: The token is stored in plain text. (Default)
* **`hashed`**: The token is hashed using the default hasher.
* **`custom-hasher`**: A custom hasher function that takes a token and returns a hashed token.
Note: It will not affect the token that's sent, it will only affect the token stored in your database.
Examples:
```ts title="No hashing (default)"
oneTimeToken({
storeToken: "plain"
})
```
```ts title="built-in hasher"
oneTimeToken({
storeToken: "hashed"
})
```
```ts title="custom hasher"
oneTimeToken({
storeToken: {
type: "custom-hasher",
hash: async (token) => {
return myCustomHasher(token);
}
}
})
```
# plugins: Open API
URL: /docs/plugins/open-api
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/open-api.mdx
Open API reference for Better Auth.
***
title: Open API
description: Open API reference for Better Auth.
------------------------------------------------
This is a plugin that provides an Open API reference for Better Auth. It shows all endpoints added by plugins and the core. It also provides a way to test the endpoints. It uses [Scalar](https://scalar.com/) to display the Open API reference.
This plugin is still in the early stages of development. We are working on adding more features to it and filling in the gaps.
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { openAPI } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
openAPI(), // [!code highlight]
] // [!code highlight]
})
```
### Navigate to `/api/auth/reference` to view the Open API reference
Each plugin endpoints are grouped by the plugin name. The core endpoints are grouped under the `Default` group. And Model schemas are grouped under the `Models` group.

## Usage
The Open API reference is generated using the [OpenAPI 3.0](https://swagger.io/specification/) specification. You can use the reference to generate client libraries, documentation, and more.
The reference is generated using the [Scalar](https://scalar.com/) library. Scalar provides a way to view and test the endpoints. You can test the endpoints by clicking on the `Try it out` button and providing the required parameters.

### Generated Schema
To get the generated Open API schema directly as JSON, you can do `auth.api.generateOpenAPISchema()`. This will return the Open API schema as a JSON object.
```ts
import { auth } from "~/lib/auth"
const openAPISchema = await auth.api.generateOpenAPISchema()
console.log(openAPISchema)
```
### Using Scalar with Multiple Sources
If you're using Scalar for your API documentation, you can add Better Auth as an additional source alongside your main API:
When using Hono with Scalar for OpenAPI documentation, you can integrate Better Auth by adding it as a source:
```ts
app.get("/docs", Scalar({
pageTitle: "API Documentation",
sources: [
{ url: "/api/open-api", title: "API" },
// Better Auth schema generation endpoint
{ url: "/api/auth/open-api/generate-schema", title: "Auth" },
],
}));
```
## Configuration
`path` - The path where the Open API reference is served. Default is `/api/auth/reference`. You can change it to any path you like, but keep in mind that it will be appended to the base path of your auth server.
`disableDefaultReference` - If set to `true`, the default Open API reference UI by Scalar will be disabled. Default is `false`.
This allows you to display both your application's API and Better Auth's authentication endpoints in a unified documentation interface.
`theme` - Allows you to change the theme of the OpenAPI reference page. Default is `default`.
# plugins: Organization
URL: /docs/plugins/organization
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/organization.mdx
The organization plugin allows you to manage your organization's members and teams.
***
title: Organization
description: The organization plugin allows you to manage your organization's members and teams.
------------------------------------------------------------------------------------------------
Organizations simplifies user access and permissions management. Assign roles and permissions to streamline project management, team coordination, and partnerships.
## Installation
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
organization() // [!code highlight]
] // [!code highlight]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { organizationClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
organizationClient() // [!code highlight]
] // [!code highlight]
})
```
## Usage
Once you've installed the plugin, you can start using the organization plugin to manage your organization's members and teams. The client plugin will provide you with methods under the `organization` namespace, and the server `api` will provide you with the necessary endpoints to manage your organization and give you an easier way to call the functions on your own backend.
## Organization
### Create an organization
### Client Side
```ts
const { data, error } = await authClient.organization.create({
name: My Organization,
slug: my-org,
logo: https://example.com/logo.png, // required
metadata, // required
userId: some_user_id, // required
keepCurrentActiveOrganization, // required
});
```
### Server Side
```ts
const data = await auth.api.createOrganization({
body: {
name: My Organization,
slug: my-org,
logo: https://example.com/logo.png, // required
metadata, // required
userId: some_user_id, // required
keepCurrentActiveOrganization, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type createOrganization = {
/**
* The organization name.
*/
name: string = "My Organization"
/**
* The organization slug.
*/
slug: string = "my-org"
/**
* The organization logo.
*/
logo?: string = "https://example.com/logo.png"
/**
* The metadata of the organization.
*/
metadata?: Record
/**
* The user ID of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server.
* @serverOnly
*/
userId?: string = "some_user_id"
/**
* Whether to keep the current active organization active after creating a new one.
*/
keepCurrentActiveOrganization?: boolean = false
}
```
#### Restrict who can create an organization
By default, any user can create an organization. To restrict this, set the `allowUserToCreateOrganization` option to a function that returns a boolean, or directly to `true` or `false`.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
const auth = betterAuth({
//...
plugins: [
organization({
allowUserToCreateOrganization: async (user) => {
// [!code highlight]
const subscription = await getSubscription(user.id); // [!code highlight]
return subscription.plan === "pro"; // [!code highlight]
}, // [!code highlight]
}),
],
});
```
#### Check if organization slug is taken
To check if an organization slug is taken or not you can use the `checkSlug` function provided by the client. The function takes an object with the following properties:
### Client Side
```ts
const { data, error } = await authClient.organization.checkSlug({
slug: my-org,
});
```
### Server Side
```ts
const data = await auth.api.checkOrganizationSlug({
body: {
slug: my-org,
}
});
```
### Type Definition
```ts
type checkOrganizationSlug = {
/**
* The organization slug to check.
*/
slug: string = "my-org"
}
```
### Organization Hooks
You can customize organization operations using hooks that run before and after various organization-related activities. Better Auth provides two ways to configure hooks:
1. **Legacy organizationCreation hooks** (deprecated, use `organizationHooks` instead)
2. **Modern organizationHooks** (recommended) - provides comprehensive control over all organization-related activities
#### Organization Creation and Management Hooks
Control organization lifecycle operations:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
organizationHooks: {
// Organization creation hooks
beforeCreateOrganization: async ({ organization, user }) => {
// Run custom logic before organization is created
// Optionally modify the organization data
return {
data: {
...organization,
metadata: {
customField: "value",
},
},
};
},
afterCreateOrganization: async ({ organization, member, user }) => {
// Run custom logic after organization is created
// e.g., create default resources, send notifications
await setupDefaultResources(organization.id);
},
// Organization update hooks
beforeUpdateOrganization: async ({ organization, user, member }) => {
// Validate updates, apply business rules
return {
data: {
...organization,
name: organization.name?.toLowerCase(),
},
};
},
afterUpdateOrganization: async ({ organization, user, member }) => {
// Sync changes to external systems
await syncOrganizationToExternalSystems(organization);
},
},
}),
],
});
```
The legacy `organizationCreation` hooks are still supported but deprecated.
Use `organizationHooks.beforeCreateOrganization` and
`organizationHooks.afterCreateOrganization` instead for new projects.
#### Member Hooks
Control member operations within organizations:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
organizationHooks: {
// Before a member is added to an organization
beforeAddMember: async ({ member, user, organization }) => {
// Custom validation or modification
console.log(`Adding ${user.email} to ${organization.name}`);
// Optionally modify member data
return {
data: {
...member,
role: "custom-role", // Override the role
},
};
},
// After a member is added
afterAddMember: async ({ member, user, organization }) => {
// Send welcome email, create default resources, etc.
await sendWelcomeEmail(user.email, organization.name);
},
// Before a member is removed
beforeRemoveMember: async ({ member, user, organization }) => {
// Cleanup user's resources, send notification, etc.
await cleanupUserResources(user.id, organization.id);
},
// After a member is removed
afterRemoveMember: async ({ member, user, organization }) => {
await logMemberRemoval(user.id, organization.id);
},
// Before updating a member's role
beforeUpdateMemberRole: async ({
member,
newRole,
user,
organization,
}) => {
// Validate role change permissions
if (newRole === "owner" && !hasOwnerUpgradePermission(user)) {
throw new Error("Cannot upgrade to owner role");
}
// Optionally modify the role
return {
data: {
role: newRole,
},
};
},
// After updating a member's role
afterUpdateMemberRole: async ({
member,
previousRole,
user,
organization,
}) => {
await logRoleChange(user.id, previousRole, member.role);
},
},
}),
],
});
```
#### Invitation Hooks
Control invitation lifecycle:
```ts title="auth.ts"
export const auth = betterAuth({
plugins: [
organization({
organizationHooks: {
// Before creating an invitation
beforeCreateInvitation: async ({
invitation,
inviter,
organization,
}) => {
// Custom validation or expiration logic
const customExpiration = new Date(
Date.now() + 1000 * 60 * 60 * 24 * 7
); // 7 days
return {
data: {
...invitation,
expiresAt: customExpiration,
},
};
},
// After creating an invitation
afterCreateInvitation: async ({
invitation,
inviter,
organization,
}) => {
// Send custom invitation email, track metrics, etc.
await sendCustomInvitationEmail(invitation, organization);
},
// Before accepting an invitation
beforeAcceptInvitation: async ({ invitation, user, organization }) => {
// Additional validation before acceptance
await validateUserEligibility(user, organization);
},
// After accepting an invitation
afterAcceptInvitation: async ({
invitation,
member,
user,
organization,
}) => {
// Setup user account, assign default resources
await setupNewMemberResources(user, organization);
},
// Before/after rejecting invitations
beforeRejectInvitation: async ({ invitation, user, organization }) => {
// Log rejection reason, send notification to inviter
},
afterRejectInvitation: async ({ invitation, user, organization }) => {
await notifyInviterOfRejection(invitation.inviterId, user.email);
},
// Before/after cancelling invitations
beforeCancelInvitation: async ({
invitation,
cancelledBy,
organization,
}) => {
// Verify cancellation permissions
},
afterCancelInvitation: async ({
invitation,
cancelledBy,
organization,
}) => {
await logInvitationCancellation(invitation.id, cancelledBy.id);
},
},
}),
],
});
```
#### Team Hooks
Control team operations (when teams are enabled):
```ts title="auth.ts"
export const auth = betterAuth({
plugins: [
organization({
teams: { enabled: true },
organizationHooks: {
// Before creating a team
beforeCreateTeam: async ({ team, user, organization }) => {
// Validate team name, apply naming conventions
return {
data: {
...team,
name: team.name.toLowerCase().replace(/\s+/g, "-"),
},
};
},
// After creating a team
afterCreateTeam: async ({ team, user, organization }) => {
// Create default team resources, channels, etc.
await createDefaultTeamResources(team.id);
},
// Before updating a team
beforeUpdateTeam: async ({ team, updates, user, organization }) => {
// Validate updates, apply business rules
return {
data: {
...updates,
name: updates.name?.toLowerCase(),
},
};
},
// After updating a team
afterUpdateTeam: async ({ team, user, organization }) => {
await syncTeamChangesToExternalSystems(team);
},
// Before deleting a team
beforeDeleteTeam: async ({ team, user, organization }) => {
// Backup team data, notify members
await backupTeamData(team.id);
},
// After deleting a team
afterDeleteTeam: async ({ team, user, organization }) => {
await cleanupTeamResources(team.id);
},
// Team member operations
beforeAddTeamMember: async ({
teamMember,
team,
user,
organization,
}) => {
// Validate team membership limits, permissions
const memberCount = await getTeamMemberCount(team.id);
if (memberCount >= 10) {
throw new Error("Team is full");
}
},
afterAddTeamMember: async ({
teamMember,
team,
user,
organization,
}) => {
await grantTeamAccess(user.id, team.id);
},
beforeRemoveTeamMember: async ({
teamMember,
team,
user,
organization,
}) => {
// Backup user's team-specific data
await backupTeamMemberData(user.id, team.id);
},
afterRemoveTeamMember: async ({
teamMember,
team,
user,
organization,
}) => {
await revokeTeamAccess(user.id, team.id);
},
},
}),
],
});
```
#### Hook Error Handling
All hooks support error handling. Throwing an error in a `before` hook will prevent the operation from proceeding:
```ts title="auth.ts"
import { APIError } from "better-auth/api";
export const auth = betterAuth({
plugins: [
organization({
organizationHooks: {
beforeAddMember: async ({ member, user, organization }) => {
// Check if user has pending violations
const violations = await checkUserViolations(user.id);
if (violations.length > 0) {
throw new APIError("BAD_REQUEST", {
message:
"User has pending violations and cannot join organizations",
});
}
},
beforeCreateTeam: async ({ team, user, organization }) => {
// Validate team name uniqueness
const existingTeam = await findTeamByName(team.name, organization.id);
if (existingTeam) {
throw new APIError("BAD_REQUEST", {
message: "Team name already exists in this organization",
});
}
},
},
}),
],
});
```
### List User's Organizations
To list the organizations that a user is a member of, you can use `useListOrganizations` hook. It implements a reactive way to get the organizations that the user is a member of.
```tsx title="client.tsx"
import { authClient } from "@/lib/auth-client"
function App(){
const { data: organizations } = authClient.useListOrganizations()
return (
{organizations.map((org) => (
{org.name}
))}
)
}
```
```svelte title="page.svelte"
Organizations
{#if $organizations.isPending}
Loading...
{:else if !$organizations.data?.length}
No organizations found.
{:else}
{#each $organizations.data as organization}
{organization.name}
{/each}
{/if}
```
```vue title="organization.vue"
Organizations
Loading...
No organizations found.
{{ organization.name }}
```
Or alternatively, you can call `organization.list` if you don't want to use a hook.
### Client Side
```ts
const { data, error } = await authClient.organization.list({});
```
### Server Side
```ts
const data = await auth.api.listOrganizations({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listOrganizations = {
}
```
### Active Organization
Active organization is the workspace the user is currently working on. By default when the user is signed in the active organization is set to `null`. You can set the active organization to the user session.
It's not always you want to persist the active organization in the session.
You can manage the active organization in the client side only. For example,
multiple tabs can have different active organizations.
#### Set Active Organization
You can set the active organization by calling the `organization.setActive` function. It'll set the active organization for the user session.
In some applications, you may want the ability to unset an active
organization. In this case, you can call this endpoint with `organizationId`
set to `null`.
### Client Side
```ts
const { data, error } = await authClient.organization.setActive({
organizationId: org-id, // required
organizationSlug: org-slug, // required
});
```
### Server Side
```ts
const data = await auth.api.setActiveOrganization({
body: {
organizationId: org-id, // required
organizationSlug: org-slug, // required
}
});
```
### Type Definition
```ts
type setActiveOrganization = {
/**
* The organization ID to set as active. It can be null to unset the active organization.
*/
organizationId?: string | null = "org-id"
/**
* The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided.
*/
organizationSlug?: string = "org-slug"
}
```
To set active organization when a session is created you can use [database hooks](/docs/concepts/database#database-hooks).
```ts title="auth.ts"
export const auth = betterAuth({
databaseHooks: {
session: {
create: {
before: async (session) => {
const organization = await getActiveOrganization(session.userId);
return {
data: {
...session,
activeOrganizationId: organization.id,
},
};
},
},
},
},
});
```
#### Use Active Organization
To retrieve the active organization for the user, you can call the `useActiveOrganization` hook. It returns the active organization for the user. Whenever the active organization changes, the hook will re-evaluate and return the new active organization.
```tsx title="client.tsx"
import { authClient } from "@/lib/auth-client"
function App(){
const { data: activeOrganization } = authClient.useActiveOrganization()
return (
{activeOrganization ?
{activeOrganization.name}
: null}
)
}
```
```tsx title="client.tsx"
Active Organization
{#if $activeOrganization.isPending}
Loading...
{:else if $activeOrganization.data === null}
No active organization found.
{:else}
{$activeOrganization.data.name}
{/if}
```
```vue title="organization.vue"
Active organization
Loading...
No active organization.
{{ activeOrganization.data.name }}
```
### Get Full Organization
To get the full details of an organization, you can use the `getFullOrganization` function.
By default, if you don't pass any properties, it will use the active organization.
### Client Side
```ts
const { data, error } = await authClient.organization.getFullOrganization({
organizationId: org-id, // required
organizationSlug: org-slug, // required
membersLimit, // required
});
```
### Server Side
```ts
const data = await auth.api.getFullOrganization({
query: {
organizationId: org-id, // required
organizationSlug: org-slug, // required
membersLimit, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getFullOrganization = {
/**
* The organization ID to get. By default, it will use the active organization.
*/
organizationId?: string = "org-id"
/**
* The organization slug to get.
*/
organizationSlug?: string = "org-slug"
/**
* The limit of members to get. By default, it uses the membershipLimit option which defaults to 100.
*/
membersLimit?: number = 100
}
```
### Update Organization
To update organization info, you can use `organization.update`
### Client Side
```ts
const { data, error } = await authClient.organization.update({
data,
name: updated-name, // required
slug: updated-slug, // required
logo: new-logo.url, // required
metadata, // required
});
```
### Server Side
```ts
const data = await auth.api.updateOrganization({
body: {
data,
name: updated-name, // required
slug: updated-slug, // required
logo: new-logo.url, // required
metadata, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type updateOrganization = {
/**
* A partial list of data to update the organization.
*/
data: {
/**
* The name of the organization.
*/
name?: string = "updated-name"
/**
* The slug of the organization.
*/
slug?: string = "updated-slug"
/**
* The logo of the organization.
*/
logo?: string = "new-logo.url"
/**
* The metadata of the organization.
*/
metadata?: Record | null = { customerId: "test"
}
```
### Delete Organization
To remove user owned organization, you can use `organization.delete`
### Client Side
```ts
const { data, error } = await authClient.organization.delete({
organizationId: org-id,
});
```
### Server Side
```ts
const data = await auth.api.deleteOrganization({
body: {
organizationId: org-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type deleteOrganization = {
/*
* The organization ID to delete.
*/
organizationId: string = "org-id"
}
```
If the user has the necessary permissions (by default: role is owner) in the specified organization, all members, invitations and organization information will be removed.
You can configure how organization deletion is handled through `organizationDeletion` option:
```ts
const auth = betterAuth({
plugins: [
organization({
disableOrganizationDeletion: true, //to disable it altogether
organizationHooks: {
beforeDeleteOrganization: async (data, request) => {
// a callback to run before deleting org
},
afterDeleteOrganization: async (data, request) => {
// a callback to run after deleting org
},
},
}),
],
});
```
## Invitations
To add a member to an organization, we first need to send an invitation to the user. The user will receive an email/sms with the invitation link. Once the user accepts the invitation, they will be added to the organization.
### Setup Invitation Email
For member invitation to work we first need to provide `sendInvitationEmail` to the `better-auth` instance. This function is responsible for sending the invitation email to the user.
You'll need to construct and send the invitation link to the user. The link should include the invitation ID, which will be used with the acceptInvitation function when the user clicks on it.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendOrganizationInvitation } from "./email";
export const auth = betterAuth({
plugins: [
organization({
async sendInvitationEmail(data) {
const inviteLink = `https://example.com/accept-invitation/${data.id}`;
sendOrganizationInvitation({
email: data.email,
invitedByUsername: data.inviter.user.name,
invitedByEmail: data.inviter.user.email,
teamName: data.organization.name,
inviteLink,
});
},
}),
],
});
```
### Send Invitation
To invite users to an organization, you can use the `invite` function provided by the client. The `invite` function takes an object with the following properties:
### Client Side
```ts
const { data, error } = await authClient.organization.inviteMember({
email: example@gmail.com,
role: member,
organizationId: org-id, // required
resend, // required
teamId: team-id, // required
});
```
### Server Side
```ts
const data = await auth.api.createInvitation({
body: {
email: example@gmail.com,
role: member,
organizationId: org-id, // required
resend, // required
teamId: team-id, // required
}
});
```
### Type Definition
```ts
type createInvitation = {
/**
* The email address of the user to invite.
*/
email: string = "example@gmail.com"
/**
* The role(s) to assign to the user. It can be `admin`, `member`, or `guest`.
*/
role: string | string[] = "member"
/**
* The organization ID to invite the user to. Defaults to the active organization.
*/
organizationId?: string = "org-id"
/**
* Resend the invitation email, if the user is already invited.
*/
resend?: boolean = true
/**
* The team ID to invite the user to.
*/
teamId?: string = "team-id"
}
```
* If the user is already a member of the organization, the invitation will be
canceled. - If the user is already invited to the organization, unless
`resend` is set to `true`, the invitation will not be sent again. - If
`cancelPendingInvitationsOnReInvite` is set to `true`, the invitation will be
canceled if the user is already invited to the organization and a new
invitation is sent.
### Accept Invitation
When a user receives an invitation email, they can click on the invitation link to accept the invitation. The invitation link should include the invitation ID, which will be used to accept the invitation.
Make sure to call the `acceptInvitation` function after the user is logged in.
### Client Side
```ts
const { data, error } = await authClient.organization.acceptInvitation({
invitationId: invitation-id,
});
```
### Server Side
```ts
const data = await auth.api.acceptInvitation({
body: {
invitationId: invitation-id,
}
});
```
### Type Definition
```ts
type acceptInvitation = {
/**
* The ID of the invitation to accept.
*/
invitationId: string = "invitation-id"
}
```
#### Email Verification Requirement
If the `requireEmailVerificationOnInvitation` option is enabled in your organization configuration, users must verify their email address before they can accept invitations. This adds an extra security layer to ensure that only verified users can join your organization.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
requireEmailVerificationOnInvitation: true, // [!code highlight]
async sendInvitationEmail(data) {
// ... your email sending logic
},
}),
],
});
```
### Invitation Accepted Callback
You can configure Better Auth to execute a callback function when an invitation is accepted. This is useful for logging events, updating analytics, sending notifications, or any other custom logic you need to run when someone joins your organization.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
async sendInvitationEmail(data) {
// ... your invitation email logic
},
async onInvitationAccepted(data) {
// This callback gets triggered when an invitation is accepted
},
}),
],
});
```
The callback receives the following data:
* `id`: The invitation ID
* `role`: The role assigned to the user
* `organization`: The organization the user joined
* `invitation`: The invitation object
* `inviter`: The member who sent the invitation (including user details)
* `acceptedUser`: The user who accepted the invitation
### Cancel Invitation
If a user has sent out an invitation, you can use this method to cancel it.
If you're looking for how a user can reject an invitation, you can find that [here](#reject-invitation).
### Client Side
```ts
const { data, error } = await authClient.organization.cancelInvitation({
invitationId: invitation-id,
});
```
### Server Side
```ts
await auth.api.cancelInvitation({
body: {
invitationId: invitation-id,
}
});
```
### Type Definition
```ts
type cancelInvitation = {
/**
* The ID of the invitation to cancel.
*/
invitationId: string = "invitation-id"
}
```
### Reject Invitation
If this user has received an invitation, but wants to decline it, this method will allow you to do so by rejecting it.
### Client Side
```ts
const { data, error } = await authClient.organization.rejectInvitation({
invitationId: invitation-id,
});
```
### Server Side
```ts
await auth.api.rejectInvitation({
body: {
invitationId: invitation-id,
}
});
```
### Type Definition
```ts
type rejectInvitation = {
/**
* The ID of the invitation to reject.
*/
invitationId: string = "invitation-id"
}
```
Like accepting invitations, rejecting invitations also requires email
verification when the `requireEmailVerificationOnInvitation` option is
enabled. Users with unverified emails will receive an error when attempting to
reject invitations.
### Get Invitation
To get an invitation you can use the `organization.getInvitation` function provided by the client. You need to provide the invitation id as a query parameter.
### Client Side
```ts
const { data, error } = await authClient.organization.getInvitation({
id: invitation-id,
});
```
### Server Side
```ts
const data = await auth.api.getInvitation({
query: {
id: invitation-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getInvitation = {
/**
* The ID of the invitation to get.
*/
id: string = "invitation-id"
}
```
### List Invitations
To list all invitations for a given organization you can use the `listInvitations` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.organization.listInvitations({
organizationId: organization-id, // required
});
```
### Server Side
```ts
const data = await auth.api.listInvitations({
query: {
organizationId: organization-id, // required
}
});
```
### Type Definition
```ts
type listInvitations = {
/**
* An optional ID of the organization to list invitations for. If not provided, will default to the user's active organization.
*/
organizationId?: string = "organization-id"
}
```
### List user invitations
To list all invitations for a given user you can use the `listUserInvitations` function provided by the client.
```ts title="auth-client.ts"
const invitations = await authClient.organization.listUserInvitations();
```
On the server, you can pass the user ID as a query parameter.
```ts title="api.ts"
const invitations = await auth.api.listUserInvitations({
query: {
email: "user@example.com",
},
});
```
The `email` query parameter is only available on the server to query for
invitations for a specific user.
## Members
### List Members
To list all members of an organization you can use the `listMembers` function.
### Client Side
```ts
const { data, error } = await authClient.organization.listMembers({
organizationId: organization-id, // required
limit, // required
offset, // required
sortBy: createdAt, // required
sortDirection: desc, // required
filterField: createdAt, // required
filterOperator: eq, // required
filterValue: value, // required
});
```
### Server Side
```ts
const data = await auth.api.listMembers({
query: {
organizationId: organization-id, // required
limit, // required
offset, // required
sortBy: createdAt, // required
sortDirection: desc, // required
filterField: createdAt, // required
filterOperator: eq, // required
filterValue: value, // required
}
});
```
### Type Definition
```ts
type listMembers = {
/**
* An optional organization ID to list members for. If not provided, will default to the user's active organization.
*/
organizationId?: string = "organization-id"
/**
* The limit of members to return.
*/
limit?: number = 100
/**
* The offset to start from.
*/
offset?: number = 0
/**
* The field to sort by.
*/
sortBy?: string = "createdAt"
/**
* The direction to sort by.
*/
sortDirection?: "asc" | "desc" = "desc"
/**
* The field to filter by.
*/
filterField?: string = "createdAt"
/**
* The operator to filter by.
*/
filterOperator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "contains" = "eq"
/**
* The value to filter by.
*/
filterValue?: string = "value"
}
```
### Remove Member
To remove you can use `organization.removeMember`
### Client Side
```ts
const { data, error } = await authClient.organization.removeMember({
memberIdOrEmail: user@example.com,
organizationId: org-id, // required
});
```
### Server Side
```ts
const data = await auth.api.removeMember({
body: {
memberIdOrEmail: user@example.com,
organizationId: org-id, // required
}
});
```
### Type Definition
```ts
type removeMember = {
/**
* The ID or email of the member to remove.
*/
memberIdOrEmail: string = "user@example.com"
/**
* The ID of the organization to remove the member from. If not provided, the active organization will be used.
*/
organizationId?: string = "org-id"
}
```
### Update Member Role
To update the role of a member in an organization, you can use the `organization.updateMemberRole`. If the user has the permission to update the role of the member, the role will be updated.
### Client Side
```ts
const { data, error } = await authClient.organization.updateMemberRole({
role,
memberId: member-id,
organizationId: organization-id, // required
});
```
### Server Side
```ts
await auth.api.updateMemberRole({
body: {
role,
memberId: member-id,
organizationId: organization-id, // required
}
});
```
### Type Definition
```ts
type updateMemberRole = {
/**
* The new role to be applied. This can be a string or array of strings representing the roles.
*/
role: string | string[] = ["admin", "sale"]
/**
* The member id to apply the role update to.
*/
memberId: string = "member-id"
/**
* An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization.
*/
organizationId?: string = "organization-id"
}
```
### Get Active Member
To get the current member of the active organization you can use the `organization.getActiveMember` function. This function will return the user's member details in their active organization.
### Client Side
```ts
const { data, error } = await authClient.organization.getActiveMember({});
```
### Server Side
```ts
const member = await auth.api.getActiveMember({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getActiveMember = {
}
```
### Get Active Member Role
To get the current role member of the active organization you can use the `organization.getActiveMemberRole` function. This function will return the user's member role in their active organization.
### Client Side
```ts
const { data, error } = await authClient.organization.getActiveMemberRole({});
```
### Server Side
```ts
const { role } = await auth.api.getActiveMemberRole({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getActiveMemberRole = {
}
```
### Add Member
If you want to add a member directly to an organization without sending an invitation, you can use the `addMember` function which can only be invoked on the server.
### Client Side
```ts
const { data, error } = await authClient.organization.addMember({
userId: user-id, // required
role,
organizationId: org-id, // required
teamId: team-id, // required
});
```
### Server Side
```ts
const data = await auth.api.addMember({
body: {
userId: user-id, // required
role,
organizationId: org-id, // required
teamId: team-id, // required
}
});
```
### Type Definition
```ts
type addMember = {
/**
* The user ID which represents the user to be added as a member. If `null` is provided, then it's expected to provide session headers.
*/
userId?: string | null = "user-id"
/**
* The role(s) to assign to the new member.
*/
role: string | string[] = ["admin", "sale"]
/**
* An optional organization ID to pass. If not provided, will default to the user's active organization.
*/
organizationId?: string = "org-id"
/**
* An optional team ID to add the member to.
*/
teamId?: string = "team-id"
}
```
### Leave Organization
To leave organization you can use `organization.leave` function. This function will remove the current user from the organization.
### Client Side
```ts
const { data, error } = await authClient.organization.leave({
organizationId: organization-id,
});
```
### Server Side
```ts
await auth.api.leaveOrganization({
body: {
organizationId: organization-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type leaveOrganization = {
/**
* The organization ID for the member to leave.
*/
organizationId: string = "organization-id"
}
```
## Access Control
The organization plugin provides a very flexible access control system. You can control the access of the user based on the role they have in the organization. You can define your own set of permissions based on the role of the user.
### Roles
By default, there are three roles in the organization:
`owner`: The user who created the organization by default. The owner has full control over the organization and can perform any action.
`admin`: Users with the admin role have full control over the organization except for deleting the organization or changing the owner.
`member`: Users with the member role have limited control over the organization. They can create projects, invite users, and manage projects they have created.
A user can have multiple roles. Multiple roles are stored as string separated
by comma (",").
### Permissions
By default, there are three resources, and these have two to three actions.
**organization**:
`update` `delete`
**member**:
`create` `update` `delete`
**invitation**:
`create` `cancel`
The owner has full control over all the resources and actions. The admin has full control over all the resources except for deleting the organization or changing the owner. The member has no control over any of those actions other than reading the data.
### Custom Permissions
The plugin provides an easy way to define your own set of permissions for each role.
#### Create Access Control
You first need to create access controller by calling `createAccessControl` function and passing the statement object. The statement object should have the resource name as the key and the array of actions as the value.
```ts title="permissions.ts"
import { createAccessControl } from "better-auth/plugins/access";
/**
* make sure to use `as const` so typescript can infer the type correctly
*/
const statement = { // [!code highlight]
project: ["create", "share", "update", "delete"], // [!code highlight]
} as const; // [!code highlight]
const ac = createAccessControl(statement); // [!code highlight]
```
#### Create Roles
Once you have created the access controller you can create roles with the permissions you have defined.
```ts title="permissions.ts"
import { createAccessControl } from "better-auth/plugins/access";
const statement = {
project: ["create", "share", "update", "delete"],
} as const;
const ac = createAccessControl(statement);
const member = ac.newRole({ // [!code highlight]
project: ["create"], // [!code highlight]
}); // [!code highlight]
const admin = ac.newRole({ // [!code highlight]
project: ["create", "update"], // [!code highlight]
}); // [!code highlight]
const owner = ac.newRole({ // [!code highlight]
project: ["create", "update", "delete"], // [!code highlight]
}); // [!code highlight]
const myCustomRole = ac.newRole({ // [!code highlight]
project: ["create", "update", "delete"], // [!code highlight]
organization: ["update"], // [!code highlight]
}); // [!code highlight]
```
When you create custom roles for existing roles, the predefined permissions for those roles will be overridden. To add the existing permissions to the custom role, you need to import `defaultStatements` and merge it with your new statement, plus merge the roles' permissions set with the default roles.
```ts title="permissions.ts"
import { createAccessControl } from "better-auth/plugins/access";
import { defaultStatements, adminAc } from 'better-auth/plugins/organization/access'
const statement = {
...defaultStatements, // [!code highlight]
project: ["create", "share", "update", "delete"],
} as const;
const ac = createAccessControl(statement);
const admin = ac.newRole({
project: ["create", "update"],
...adminAc.statements, // [!code highlight]
});
```
#### Pass Roles to the Plugin
Once you have created the roles you can pass them to the organization plugin both on the client and the server.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"
import { ac, owner, admin, member } from "@/auth/permissions"
export const auth = betterAuth({
plugins: [
organization({
ac,
roles: {
owner,
admin,
member,
myCustomRole
}
}),
],
});
```
You also need to pass the access controller and the roles to the client plugin.
```ts title="auth-client"
import { createAuthClient } from "better-auth/client"
import { organizationClient } from "better-auth/client/plugins"
import { ac, owner, admin, member, myCustomRole } from "@/auth/permissions"
export const authClient = createAuthClient({
plugins: [
organizationClient({
ac,
roles: {
owner,
admin,
member,
myCustomRole
}
})
]
})
```
### Access Control Usage
**Has Permission**:
You can use the `hasPermission` action provided by the `api` to check the permission of the user.
```ts title="api.ts"
import { auth } from "@/auth";
await auth.api.hasPermission({
headers: await headers(),
body: {
permissions: {
project: ["create"], // This must match the structure in your access control
},
},
});
// You can also check multiple resource permissions at the same time
await auth.api.hasPermission({
headers: await headers(),
body: {
permissions: {
project: ["create"], // This must match the structure in your access control
sale: ["create"],
},
},
});
```
If you want to check the permission of the user on the client from the server you can use the `hasPermission` function provided by the client.
```ts title="auth-client.ts"
const canCreateProject = await authClient.organization.hasPermission({
permissions: {
project: ["create"],
},
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale =
await authClient.organization.hasPermission({
permissions: {
project: ["create"],
sale: ["create"],
},
});
```
**Check Role Permission**:
Once you have defined the roles and permissions to avoid checking the permission from the server you can use the `checkRolePermission` function provided by the client.
```ts title="auth-client.ts"
const canCreateProject = authClient.organization.checkRolePermission({
permissions: {
organization: ["delete"],
},
role: "admin",
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale =
authClient.organization.checkRolePermission({
permissions: {
organization: ["delete"],
member: ["delete"],
},
role: "admin",
});
```
This will not include any dynamic roles as everything is ran syncronously on the client side.
Please use the [hasPermission](#access-control-usage) APIs to include checks for any dynamic roles & permissions.
***
## Dynamic Access Control
Dynamic access control allows you to create roles at runtime for organizations. This is achieved by storing the
created roles and permissions associated with an organization in a database table.
### Enabling Dynamic Access Control
To enable dynamic access control, pass the `dynamicAccessControl` configuration option with `enabled` set to `true` to both server and client plugins.
Ensure you have pre-defined an `ac` instance on the server auth plugin.
This is important as this is how we can infer the permissions that are available for use.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { ac } from "@/auth/permissions";
export const auth = betterAuth({
plugins: [ // [!code highlight]
organization({ // [!code highlight]
ac, // Must be defined in order for dynamic access control to work // [!code highlight]
dynamicAccessControl: { // [!code highlight]
enabled: true, // [!code highlight]
}, // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
```
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
organizationClient({ // [!code highlight]
dynamicAccessControl: { // [!code highlight]
enabled: true, // [!code highlight]
}, // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
```
This will require you to run migrations to add the new `organizationRole` table to the database.
The `authClient.organization.checkRolePermission` function will not include any dynamic roles as everything is ran syncronously on the client side.
Please use the [hasPermission](#access-control-usage) APIs to include checks for any dynamic roles.
### Creating a role
To create a new role for an organization at runtime, you can use the `createRole` function.
Only users with roles which contain the `ac` resource with the `create` permission can create a new role.
By default, only the `admin` and `owner` roles have this permission. You also cannot add permissions that your
current role in that organization can't already access.
TIP: You can validate role names by using the `dynamicAccessControl.validateRoleName` option in the organization plugin config.
Learn more [here](#validaterolename).
### Client Side
```ts
const { data, error } = await authClient.organization.createRole({
role: my-unique-role,
permission, // required
organizationId: organization-id, // required
});
```
### Server Side
```ts
await auth.api.createOrgRole({
body: {
role: my-unique-role,
permission, // required
organizationId: organization-id, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type createOrgRole = {
/**
* A unique name of the role to create.
*/
role: string = "my-unique-role"
/**
* The permissions to assign to the role.
*/
permission?: Record = permission,
/**
* The organization ID which the role will be created in. Defaults to the active organization.
*/
organizationId?: string = "organization-id"
}
```
Now you can freely call [`updateMemberRole`](#updating-a-member-role) to update the role of a member with your newly created role!
### Deleting a role
To delete a role, you can use the `deleteRole` function, then provide either a `roleName` or `roleId` parameter along
with the `organizationId` parameter.
### Client Side
```ts
const { data, error } = await authClient.organization.deleteRole({
roleName: my-role, // required
roleId: role-id, // required
organizationId: organization-id, // required
});
```
### Server Side
```ts
await auth.api.deleteOrgRole({
body: {
roleName: my-role, // required
roleId: role-id, // required
organizationId: organization-id, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type deleteOrgRole = {
/**
* The name of the role to delete. Alternatively, you can pass a `roleId` parameter instead.
*/
roleName?: string = "my-role"
/**
* The id of the role to delete. Alternatively, you can pass a `roleName` parameter instead.
*/
roleId?: string = "role-id"
/**
* The organization ID which the role will be deleted in. Defaults to the active organization.
*/
organizationId?: string = "organization-id"
}
```
### Listing roles
To list roles, you can use the `listOrgRoles` function.
This requires the `ac` resource with the `read` permission for the member to be able to list roles.
### Client Side
```ts
const { data, error } = await authClient.organization.listRoles({
organizationId: organization-id, // required
});
```
### Server Side
```ts
const roles = await auth.api.listOrgRoles({
query: {
organizationId: organization-id, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listOrgRoles = {
/**
* The organization ID which the roles are under to list. Defaults to the user's active organization.
*/
organizationId?: string = "organization-id"
}
```
### Getting a specific role
To get a specific role, you can use the `getOrgRole` function and pass either a `roleName` or `roleId` parameter.
This requires the `ac` resource with the `read` permission for the member to be able to get a role.
### Client Side
```ts
const { data, error } = await authClient.organization.getRole({
roleName: my-role, // required
roleId: role-id, // required
organizationId: organization-id, // required
});
```
### Server Side
```ts
const role = await auth.api.getOrgRole({
query: {
roleName: my-role, // required
roleId: role-id, // required
organizationId: organization-id, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type getOrgRole = {
/**
* The name of the role to get. Alternatively, you can pass a `roleId` parameter instead.
*/
roleName?: string = "my-role"
/**
* The id of the role to get. Alternatively, you can pass a `roleName` parameter instead.
*/
roleId?: string = "role-id"
/**
* The organization ID which the role will be deleted in. Defaults to the active organization.
*/
organizationId?: string = "organization-id"
}
```
### Updating a role
To update a role, you can use the `updateOrgRole` function and pass either a `roleName` or `roleId` parameter.
### Client Side
```ts
const { data, error } = await authClient.organization.updateRole({
roleName: my-role, // required
roleId: role-id, // required
organizationId: organization-id, // required
data,
permission, // required
});
```
### Server Side
```ts
const updatedRole = await auth.api.updateOrgRole({
body: {
roleName: my-role, // required
roleId: role-id, // required
organizationId: organization-id, // required
data,
permission, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type updateOrgRole = {
/**
* The name of the role to update. Alternatively, you can pass a `roleId` parameter instead.
*/
roleName?: string = "my-role"
/**
* The id of the role to update. Alternatively, you can pass a `roleName` parameter instead.
*/
roleId?: string = "role-id"
/**
* The organization ID which the role will be updated in. Defaults to the active organization.
*/
organizationId?: string = "organization-id"
/**
* The data which will be updated
*/
data: {
/**
* Optionally update the permissions of the role.
*/
permission?: Record = { project: ["create", "update", "delete"]
}
```
### Configuration Options
Below is a list of options that can be passed to the `dynamicAccessControl` object.
#### `enabled`
This option is used to enable or disable dynamic access control. By default, it is disabled.
```ts
organization({
dynamicAccessControl: {
enabled: true // [!code highlight]
}
})
```
#### `maximumRolesPerOrganization`
This option is used to limit the number of roles that can be created for an organization.
By default, the maximum number of roles that can be created for an organization is infinite.
```ts
organization({
dynamicAccessControl: {
maximumRolesPerOrganization: 10 // [!code highlight]
}
})
```
You can also pass a function that returns a number.
```ts
organization({
dynamicAccessControl: {
maximumRolesPerOrganization: async (organizationId) => { // [!code highlight]
const organization = await getOrganization(organizationId); // [!code highlight]
return organization.plan === "pro" ? 100 : 10; // [!code highlight]
} // [!code highlight]
}
})
```
### Additional Fields
To add additional fields to the `organizationRole` table, you can pass the `additionalFields` configuration option to the `organization` plugin.
```ts
organization({
schema: {
organizationRole: {
additionalFields: {
// Role colors!
color: {
type: "string",
defaultValue: "#ffffff",
},
//... other fields
},
},
},
})
```
Then, if you don't already use `inferOrgAdditionalFields` to infer the additional fields, you can use it to infer the additional fields.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { organizationClient, inferOrgAdditionalFields } from "better-auth/client/plugins"
import type { auth } from "./auth"
export const authClient = createAuthClient({
plugins: [
organizationClient({
schema: inferOrgAdditionalFields()
})
]
})
```
Otherwise, you can pass the schema values directly, the same way you do on the org plugin in the server.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { organizationClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
organizationClient({
schema: {
organizationRole: {
additionalFields: {
color: {
type: "string",
defaultValue: "#ffffff",
}
}
}
}
})
]
})
```
***
## Teams
Teams allow you to group members within an organization. The teams feature provides additional organization structure and can be used to manage permissions at a more granular level.
### Enabling Teams
To enable teams, pass the `teams` configuration option to both server and client plugins:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
teams: {
enabled: true,
maximumTeams: 10, // Optional: limit teams per organization
allowRemovingAllTeams: false, // Optional: prevent removing the last team
},
}),
],
});
```
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
organizationClient({
teams: {
enabled: true,
},
}),
],
});
```
### Managing Teams
#### Create Team
Create a new team within an organization:
### Client Side
```ts
const { data, error } = await authClient.organization.createTeam({
name: my-team,
organizationId: organization-id, // required
});
```
### Server Side
```ts
const data = await auth.api.createTeam({
body: {
name: my-team,
organizationId: organization-id, // required
}
});
```
### Type Definition
```ts
type createTeam = {
/**
* The name of the team.
*/
name: string = "my-team"
/**
* The organization ID which the team will be created in. Defaults to the active organization.
*/
organizationId?: string = "organization-id"
}
```
#### List Teams
Get all teams in an organization:
### Client Side
```ts
const { data, error } = await authClient.organization.listTeams({
query, // required
organizationId: organization-id, // required
});
```
### Server Side
```ts
const data = await auth.api.listOrganizationTeams({
query: {
query, // required
organizationId: organization-id, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listOrganizationTeams = {
/**
* Query parameters for filtering or scoping the list of teams.
*/
query?: {
/**
* The organization ID which the teams are under to list. Defaults to the user's active organization.
*/
organizationId?: string = "organization-id"
}
```
#### Update Team
Update a team's details:
### Client Side
```ts
const { data, error } = await authClient.organization.updateTeam({
teamId: team-id,
data,
name: My new team name, // required
organizationId: My new organization ID for this team, // required
createdAt, // required
updatedAt, // required
});
```
### Server Side
```ts
const data = await auth.api.updateTeam({
body: {
teamId: team-id,
data,
name: My new team name, // required
organizationId: My new organization ID for this team, // required
createdAt, // required
updatedAt, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type updateTeam = {
/**
* The ID of the team to be updated.
*/
teamId: string = "team-id"
/**
* A partial object containing options for you to update.
*/
data: {
/**
* The name of the team to be updated.
*/
name?: string = "My new team name"
/**
* The organization ID which the team falls under.
*/
organizationId?: string = "My new organization ID for this team"
/**
* The timestamp of when the team was created.
*/
createdAt?: Date = new Date()
/**
* The timestamp of when the team was last updated.
*/
updatedAt?: Date = new Date()
}
```
#### Remove Team
Delete a team from an organization:
### Client Side
```ts
const { data, error } = await authClient.organization.removeTeam({
teamId: team-id,
organizationId: organization-id, // required
});
```
### Server Side
```ts
const data = await auth.api.removeTeam({
body: {
teamId: team-id,
organizationId: organization-id, // required
}
});
```
### Type Definition
```ts
type removeTeam = {
/**
* The team ID of the team to remove.
*/
teamId: string = "team-id"
/**
* The organization ID which the team falls under. If not provided, it will default to the user's active organization.
*/
organizationId?: string = "organization-id"
}
```
#### Set Active Team
Sets the given team as the current active team. If `teamId` is `null` the current active team is unset.
### Client Side
```ts
const { data, error } = await authClient.organization.setActiveTeam({
teamId: team-id, // required
});
```
### Server Side
```ts
const data = await auth.api.setActiveTeam({
body: {
teamId: team-id, // required
}
});
```
### Type Definition
```ts
type setActiveTeam = {
/**
* The team ID of the team to set as the current active team.
*/
teamId?: string = "team-id"
}
```
#### List User Teams
List all teams that the current user is a part of.
### Client Side
```ts
const { data, error } = await authClient.organization.listUserTeams({});
```
### Server Side
```ts
const data = await auth.api.listUserTeams({});
```
### Type Definition
```ts
type listUserTeams = {
}
```
#### List Team Members
List the members of the given team.
### Client Side
```ts
const { data, error } = await authClient.organization.listTeamMembers({
teamId: team-id, // required
});
```
### Server Side
```ts
const data = await auth.api.listTeamMembers({
body: {
teamId: team-id, // required
}
});
```
### Type Definition
```ts
type listTeamMembers = {
/**
* The team whose members we should return. If this is not provided the members of the current active team get returned.
*/
teamId?: string = "team-id"
}
```
#### Add Team Member
Add a member to a team.
### Client Side
```ts
const { data, error } = await authClient.organization.addTeamMember({
teamId: team-id,
userId: user-id,
});
```
### Server Side
```ts
const data = await auth.api.addTeamMember({
body: {
teamId: team-id,
userId: user-id,
}
});
```
### Type Definition
```ts
type addTeamMember = {
/**
* The team the user should be a member of.
*/
teamId: string = "team-id"
/**
* The user ID which represents the user to be added as a member.
*/
userId: string = "user-id"
}
```
#### Remove Team Member
Remove a member from a team.
### Client Side
```ts
const { data, error } = await authClient.organization.removeTeamMember({
teamId: team-id,
userId: user-id,
});
```
### Server Side
```ts
const data = await auth.api.removeTeamMember({
body: {
teamId: team-id,
userId: user-id,
}
});
```
### Type Definition
```ts
type removeTeamMember = {
/**
* The team the user should be removed from.
*/
teamId: string = "team-id"
/**
* The user which should be removed from the team.
*/
userId: string = "user-id"
}
```
### Team Permissions
Teams follow the organization's permission system. To manage teams, users need the following permissions:
* `team:create` - Create new teams
* `team:update` - Update team details
* `team:delete` - Remove teams
By default:
* Organization owners and admins can manage teams
* Regular members cannot create, update, or delete teams
### Team Configuration Options
The teams feature supports several configuration options:
* `maximumTeams`: Limit the number of teams per organization
```ts
teams: {
enabled: true,
maximumTeams: 10 // Fixed number
// OR
maximumTeams: async ({ organizationId, session }, request) => {
// Dynamic limit based on organization plan
const plan = await getPlan(organizationId)
return plan === 'pro' ? 20 : 5
},
maximumMembersPerTeam: 10 // Fixed number
// OR
maximumMembersPerTeam: async ({ teamId, session, organizationId }, request) => {
// Dynamic limit based on team plan
const plan = await getPlan(organizationId, teamId)
return plan === 'pro' ? 50 : 10
},
}
```
* `allowRemovingAllTeams`: Control whether the last team can be removed
```ts
teams: {
enabled: true,
allowRemovingAllTeams: false // Prevent removing the last team
}
```
### Team Members
When inviting members to an organization, you can specify a team:
```ts
await authClient.organization.inviteMember({
email: "user@example.com",
role: "member",
teamId: "team-id",
});
```
The invited member will be added to the specified team upon accepting the invitation.
### Database Schema
When teams are enabled, new `team` and `teamMember` tables are added to the database.
Table Name: `team`
Table Name: `teamMember`
## Schema
The organization plugin adds the following tables to the database:
### Organization
Table Name: `organization`
### Member
Table Name: `member`
### Invitation
Table Name: `invitation`
If teams are enabled, you need to add the following fields to the invitation table:
### Session
Table Name: `session`
You need to add two more fields to the session table to store the active organization ID and the active team ID.
### Teams (optional)
Table Name: `team`
Table Name: `teamMember`
Table Name: `invitation`
### Customizing the Schema
To change the schema table name or fields, you can pass `schema` option to the organization plugin.
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
organization({
schema: {
organization: {
modelName: "organizations", //map the organization table to organizations
fields: {
name: "title", //map the name field to title
},
additionalFields: {
// Add a new field to the organization table
myCustomField: {
type: "string",
input: true,
required: false,
},
},
},
},
}),
],
});
```
#### Additional Fields
Starting with [Better Auth v1.3](https://github.com/better-auth/better-auth/releases/tag/v1.3.0), you can easily add custom fields to the `organization`, `invitation`, `member`, and `team` tables.
When you add extra fields to a model, the relevant API endpoints will automatically accept and return these new properties. For instance, if you add a custom field to the `organization` table, the `createOrganization` endpoint will include this field in its request and response payloads as needed.
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
organization({
schema: {
organization: {
additionalFields: {
myCustomField: {
// [!code highlight]
type: "string", // [!code highlight]
input: true, // [!code highlight]
required: false, // [!code highlight]
}, // [!code highlight]
},
},
},
}),
],
});
```
For inferring the additional fields, you can use the `inferOrgAdditionalFields` function. This function will infer the additional fields from the auth object type.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import {
inferOrgAdditionalFields,
organizationClient,
} from "better-auth/client/plugins";
import type { auth } from "@/auth"; // import the auth object type only
const client = createAuthClient({
plugins: [
organizationClient({
schema: inferOrgAdditionalFields(),
}),
],
});
```
if you can't import the auth object type, you can use the `inferOrgAdditionalFields` function without the generic. This function will infer the additional fields from the schema object.
```ts title="auth-client.ts"
const client = createAuthClient({
plugins: [
organizationClient({
schema: inferOrgAdditionalFields({
organization: {
// [!code highlight]
additionalFields: {
newField: {
// [!code highlight]
type: "string", // [!code highlight]
}, // [!code highlight]
},
},
}),
}),
],
});
//example usage
await client.organization.create({
name: "Test",
slug: "test",
newField: "123", //this should be allowed
//@ts-expect-error - this field is not available
unavalibleField: "123", //this should be not allowed
});
```
## Options
**allowUserToCreateOrganization**: `boolean` | `((user: User) => Promise | boolean)` - A function that determines whether a user can create an organization. By default, it's `true`. You can set it to `false` to restrict users from creating organizations.
**organizationLimit**: `number` | `((user: User) => Promise | boolean)` - The maximum number of organizations allowed for a user. By default, it's `5`. You can set it to any number you want or a function that returns a boolean.
**creatorRole**: `admin | owner` - The role of the user who creates the organization. By default, it's `owner`. You can set it to `admin`.
**membershipLimit**: `number` - The maximum number of members allowed in an organization. By default, it's `100`. You can set it to any number you want.
**sendInvitationEmail**: `async (data) => Promise` - A function that sends an invitation email to the user.
**invitationExpiresIn** : `number` - How long the invitation link is valid for in seconds. By default, it's 48 hours (2 days).
**cancelPendingInvitationsOnReInvite**: `boolean` - Whether to cancel pending invitations if the user is already invited to the organization. By default, it's `false`.
**invitationLimit**: `number` | `((user: User) => Promise | boolean)` - The maximum number of invitations allowed for a user. By default, it's `100`. You can set it to any number you want or a function that returns a boolean.
**requireEmailVerificationOnInvitation**: `boolean` - Whether to require email verification before accepting or rejecting invitations. By default, it's `false`. When enabled, users must have verified their email address before they can accept or reject organization invitations.
# plugins: Passkey
URL: /docs/plugins/passkey
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/passkey.mdx
Passkey
***
title: Passkey
description: Passkey
--------------------
Passkeys are a secure, passwordless authentication method using cryptographic key pairs, supported by WebAuthn and FIDO2 standards in web browsers. They replace passwords with unique key pairs: a private key stored on the user's device and a public key shared with the website. Users can log in using biometrics, PINs, or security keys, providing strong, phishing-resistant authentication without traditional passwords.
The passkey plugin implementation is powered by [SimpleWebAuthn](https://simplewebauthn.dev/) behind the scenes.
## Installation
### Add the plugin to your auth config
To add the passkey plugin to your auth config, you need to import the plugin and pass it to the `plugins` option of the auth instance.
**Options**
`rpID`: A unique identifier for your website. 'localhost' is okay for local dev
`rpName`: Human-readable title for your website
`origin`: The URL at which registrations and authentications should occur. `http://localhost` and `http://localhost:PORT` are also valid. Do **NOT** include any trailing /
`authenticatorSelection`: Allows customization of WebAuthn authenticator selection criteria. Leave unspecified for default settings.
* `authenticatorAttachment`: Specifies the type of authenticator
* `platform`: Authenticator is attached to the platform (e.g., fingerprint reader)
* `cross-platform`: Authenticator is not attached to the platform (e.g., security key)
* Default: `not set` (both platform and cross-platform allowed, with platform preferred)
* `residentKey`: Determines credential storage behavior.
* `required`: User MUST store credentials on the authenticator (highest security)
* `preferred`: Encourages credential storage but not mandatory
* `discouraged`: No credential storage required (fastest experience)
* Default: `preferred`
* `userVerification`: Controls biometric/PIN verification during authentication:
* `required`: User MUST verify identity (highest security)
* `preferred`: Verification encouraged but not mandatory
* `discouraged`: No verification required (fastest experience)
* Default: `preferred`
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { passkey } from "better-auth/plugins/passkey" // [!code highlight]
export const auth = betterAuth({
plugins: [ // [!code highlight]
passkey(), // [!code highlight]
], // [!code highlight]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { passkeyClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
passkeyClient() // [!code highlight]
] // [!code highlight]
})
```
## Usage
### Add/Register a passkey
To add or register a passkey make sure a user is authenticated and then call the `passkey.addPasskey` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.passkey.addPasskey({
name: example-passkey-name, // required
authenticatorAttachment: cross-platform, // required
});
```
### Server Side
```ts
const data = await auth.api.addPasskey({
body: {
name: example-passkey-name, // required
authenticatorAttachment: cross-platform, // required
}
});
```
### Type Definition
```ts
type addPasskey = {
/**
* An optional name to label the authenticator account being registered. If not provided, it will default to the user's email address or user ID
*/
name?: string = "example-passkey-name"
/**
* You can also specify the type of authenticator you want to register. Default behavior allows both platform and cross-platform passkeys
*/
authenticatorAttachment?: "platform" | "cross-platform" = "cross-platform"
}
```
### Sign in with a passkey
To sign in with a passkey you can use the `signIn.passkey` method. This will prompt the user to sign in with their passkey.
### Client Side
```ts
const { data, error } = await authClient.signIn.passkey({
email: example@gmail.com,
autoFill, // required
});
```
### Server Side
```ts
const data = await auth.api.signInPasskey({
body: {
email: example@gmail.com,
autoFill, // required
}
});
```
### Type Definition
```ts
type signInPasskey = {
/**
* The email of the user to sign in.
*/
email: string = "example@gmail.com"
/**
* Browser autofill, a.k.a. Conditional UI. Read more: https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui
*/
autoFill?: boolean = true
/**
* The URL to redirect to after the user has signed in.
*/
}
```
#### Example Usage
```ts
// With post authentication redirect
await authClient.signIn.passkey({
email: "user@example.com",
autoFill: true,
fetchOptions: {
onSuccess(context) {
// Redirect to dashboard after successful authentication
window.location.href = "/dashboard";
},
onError(context) {
// Handle authentication errors
console.error("Authentication failed:", context.error.message);
}
}
});
```
### List passkeys
You can list all of the passkeys for the authenticated user by calling `passkey.listUserPasskeys`:
### Client Side
```ts
const { data, error } = await authClient.passkey.listUserPasskeys({});
```
### Server Side
```ts
const passkeys = await auth.api.listPasskeys({
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listPasskeys = {
}
```
### Deleting passkeys
You can delete a passkey by calling `passkey.delete` and providing the passkey ID.
### Client Side
```ts
const { data, error } = await authClient.passkey.deletePasskey({
id: some-passkey-id,
});
```
### Server Side
```ts
const data = await auth.api.deletePasskey({
body: {
id: some-passkey-id,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type deletePasskey = {
/**
* The ID of the passkey to delete.
*/
id: string = "some-passkey-id"
}
```
### Updating passkey names
### Client Side
```ts
const { data, error } = await authClient.passkey.updatePasskey({
id: id of passkey,
name: my-new-passkey-name,
});
```
### Server Side
```ts
const data = await auth.api.updatePasskey({
body: {
id: id of passkey,
name: my-new-passkey-name,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type updatePasskey = {
/**
* The ID of the passkey which you want to update.
*/
id: string = "id of passkey"
/**
* The new name which the passkey will be updated to.
*/
name: string = "my-new-passkey-name"
}
```
### Conditional UI
The plugin supports conditional UI, which allows the browser to autofill the passkey if the user has already registered a passkey.
There are two requirements for conditional UI to work:
#### Update input fields
Add the `autocomplete` attribute with the value `webauthn` to your input fields. You can add this attribute to multiple input fields, but at least one is required for conditional UI to work.
The `webauthn` value should also be the last entry of the `autocomplete` attribute.
```html
```
#### Preload the passkeys
When your component mounts, you can preload the user's passkeys by calling the `authClient.signIn.passkey` method with the `autoFill` option set to `true`.
To prevent unnecessary calls, we will also add a check to see if the browser supports conditional UI.
```ts
useEffect(() => {
if (!PublicKeyCredential.isConditionalMediationAvailable ||
!PublicKeyCredential.isConditionalMediationAvailable()) {
return;
}
void authClient.signIn.passkey({ autoFill: true })
}, [])
```
Depending on the browser, a prompt will appear to autofill the passkey. If the user has multiple passkeys, they can select the one they want to use.
Some browsers also require the user to first interact with the input field before the autofill prompt appears.
### Debugging
To test your passkey implementation you can use [emulated authenticators](https://developer.chrome.com/docs/devtools/webauthn). This way you can test the registration and sign-in process without even owning a physical device.
## Schema
The plugin require a new table in the database to store passkey data.
Table Name: `passkey`
## Options
**rpID**: A unique identifier for your website. 'localhost' is okay for local dev.
**rpName**: Human-readable title for your website.
**origin**: The URL at which registrations and authentications should occur. `http://localhost` and `http://localhost:PORT` are also valid. Do NOT include any trailing /.
**authenticatorSelection**: Allows customization of WebAuthn authenticator selection criteria. When unspecified, both platform and cross-platform authenticators are allowed with `preferred` settings for `residentKey` and `userVerification`.
**aaguid**: (optional) Authenticator Attestation GUID. This is a unique identifier for the passkey provider (device or authenticator type) and can be used to identify the type of passkey device used during registration or authentication.
# plugins: Phone Number
URL: /docs/plugins/phone-number
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/phone-number.mdx
Phone number plugin
***
title: Phone Number
description: Phone number plugin
--------------------------------
The phone number plugin extends the authentication system by allowing users to sign in and sign up using their phone number. It includes OTP (One-Time Password) functionality to verify phone numbers.
## Installation
### Add Plugin to the server
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { phoneNumber } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
phoneNumber({ // [!code highlight]
sendOTP: ({ phoneNumber, code }, request) => { // [!code highlight]
// Implement sending OTP code via SMS // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { phoneNumberClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [ // [!code highlight]
phoneNumberClient() // [!code highlight]
] // [!code highlight]
})
```
## Usage
### Send OTP for Verification
To send an OTP to a user's phone number for verification, you can use the `sendVerificationCode` endpoint.
### Client Side
```ts
const { data, error } = await authClient.phoneNumber.sendOtp({
phoneNumber: +1234567890,
});
```
### Server Side
```ts
const data = await auth.api.sendPhoneNumberOTP({
body: {
phoneNumber: +1234567890,
}
});
```
### Type Definition
```ts
type sendPhoneNumberOTP = {
/**
* Phone number to send OTP.
*/
phoneNumber: string = "+1234567890"
}
```
### Verify Phone Number
After the OTP is sent, users can verify their phone number by providing the code.
### Client Side
```ts
const { data, error } = await authClient.phoneNumber.verify({
phoneNumber: +1234567890,
code: 123456,
disableSession, // required
updatePhoneNumber, // required
});
```
### Server Side
```ts
const data = await auth.api.verifyPhoneNumber({
body: {
phoneNumber: +1234567890,
code: 123456,
disableSession, // required
updatePhoneNumber, // required
}
});
```
### Type Definition
```ts
type verifyPhoneNumber = {
/**
* Phone number to verify.
*/
phoneNumber: string = "+1234567890"
/**
* OTP code.
*/
code: string = "123456"
/**
* Disable session creation after verification.
*/
disableSession?: boolean = false
/**
* Check if there is a session and update the phone number.
*/
updatePhoneNumber?: boolean = true
}
```
When the phone number is verified, the `phoneNumberVerified` field in the user table is set to `true`. If `disableSession` is not set to `true`, a session is created for the user. Additionally, if `callbackOnVerification` is provided, it will be called.
### Allow Sign-Up with Phone Number
To allow users to sign up using their phone number, you can pass `signUpOnVerification` option to your plugin configuration. It requires you to pass `getTempEmail` function to generate a temporary email for the user.
```ts title="auth.ts"
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: ({ phoneNumber, code }, request) => {
// Implement sending OTP code via SMS
},
signUpOnVerification: {
getTempEmail: (phoneNumber) => {
return `${phoneNumber}@my-site.com`
},
//optionally, you can also pass `getTempName` function to generate a temporary name for the user
getTempName: (phoneNumber) => {
return phoneNumber //by default, it will use the phone number as the name
}
}
})
]
})
```
### Sign In with Phone Number
In addition to signing in a user using send-verify flow, you can also use phone number as an identifier and sign in a user using phone number and password.
### Client Side
```ts
const { data, error } = await authClient.signIn.phoneNumber({
phoneNumber: +1234567890,
password,
rememberMe, // required
});
```
### Server Side
```ts
const data = await auth.api.signInPhoneNumber({
body: {
phoneNumber: +1234567890,
password,
rememberMe, // required
}
});
```
### Type Definition
```ts
type signInPhoneNumber = {
/**
* Phone number to sign in.
*/
phoneNumber: string = "+1234567890"
/**
* Password to use for sign in.
*/
password: string
/**
* Remember the session.
*/
rememberMe?: boolean = true
}
```
### Update Phone Number
Updating phone number uses the same process as verifying a phone number. The user will receive an OTP code to verify the new phone number.
```ts title="auth-client.ts"
await authClient.phoneNumber.sendOtp({
phoneNumber: "+1234567890" // New phone number
})
```
Then verify the new phone number with the OTP code.
```ts title="auth-client.ts"
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: "+1234567890",
code: "123456",
updatePhoneNumber: true // Set to true to update the phone number [!code highlight]
})
```
If a user session exist the phone number will be updated automatically.
### Disable Session Creation
By default, the plugin creates a session for the user after verifying the phone number. You can disable this behavior by passing `disableSession: true` to the `verify` method.
```ts title="auth-client.ts"
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: "+1234567890",
code: "123456",
disableSession: true // [!code highlight]
})
```
### Request Password Reset
To initiate a request password reset flow using `phoneNumber`, you can start by calling `requestPasswordReset` on the client to send an OTP code to the user's phone number.
### Client Side
```ts
const { data, error } = await authClient.phoneNumber.requestPasswordReset({
phoneNumber: +1234567890,
});
```
### Server Side
```ts
const data = await auth.api.requestPasswordResetPhoneNumber({
body: {
phoneNumber: +1234567890,
}
});
```
### Type Definition
```ts
type requestPasswordResetPhoneNumber = {
/**
* The phone number which is associated with the user.
*/
phoneNumber: string = "+1234567890"
}
```
Then, you can reset the password by calling `resetPassword` on the client with the OTP code and the new password.
### Client Side
```ts
const { data, error } = await authClient.phoneNumber.resetPassword({
otp: 123456,
phoneNumber: +1234567890,
newPassword: new-and-secure-password,
});
```
### Server Side
```ts
const data = await auth.api.resetPasswordPhoneNumber({
body: {
otp: 123456,
phoneNumber: +1234567890,
newPassword: new-and-secure-password,
}
});
```
### Type Definition
```ts
type resetPasswordPhoneNumber = {
/**
* The one time password to reset the password.
*/
otp: string = "123456"
/**
* The phone number to the account which intends to reset the password for.
*/
phoneNumber: string = "+1234567890"
/**
* The new password.
*/
newPassword: string = "new-and-secure-password"
}
```
## Options
* `otpLength`: The length of the OTP code to be generated. Default is `6`.
* `sendOTP`: A function that sends the OTP code to the user's phone number. It takes the phone number and the OTP code as arguments.
* `expiresIn`: The time in seconds after which the OTP code expires. Default is `300` seconds.
* `callbackOnVerification`: A function that is called after the phone number is verified. It takes the phone number and the user object as the first argument and a request object as the second argument.
```ts
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: ({ phoneNumber, code }, request) => {
// Implement sending OTP code via SMS
},
callbackOnVerification: async ({ phoneNumber, user }, request) => {
// Implement callback after phone number verification
}
})
]
})
```
* `sendPasswordResetOTP`: A function that sends the OTP code to the user's phone number for password reset. It takes the phone number and the OTP code as arguments.
* `phoneNumberValidator`: A custom function to validate the phone number. It takes the phone number as an argument and returns a boolean indicating whether the phone number is valid.
* `signUpOnVerification`: An object with the following properties:
* `getTempEmail`: A function that generates a temporary email for the user. It takes the phone number as an argument and returns the temporary email.
* `getTempName`: A function that generates a temporary name for the user. It takes the phone number as an argument and returns the temporary name.
* `requireVerification`: When enabled, users cannot sign in with their phone number until it has been verified. If an unverified user attempts to sign in, the server will respond with a 401 error (PHONE\_NUMBER\_NOT\_VERIFIED) and automatically trigger an OTP send to start the verification process.
## Schema
The plugin requires 2 fields to be added to the user table
### User Table
### OTP Verification Attempts
The phone number plugin includes a built-in protection against brute force attacks by limiting the number of verification attempts for each OTP code.
```typescript
phoneNumber({
allowedAttempts: 3, // default is 3
// ... other options
})
```
When a user exceeds the allowed number of verification attempts:
* The OTP code is automatically deleted
* Further verification attempts will return a 403 (Forbidden) status with "Too many attempts" message
* The user will need to request a new OTP code to continue
Example error response after exceeding attempts:
```json
{
"error": {
"status": 403,
"message": "Too many attempts"
}
}
```
When receiving a 403 status, prompt the user to request a new OTP code
# plugins: Polar
URL: /docs/plugins/polar
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/polar.mdx
Better Auth Plugin for Payment and Checkouts using Polar
***
title: Polar
description: Better Auth Plugin for Payment and Checkouts using Polar
---------------------------------------------------------------------
[Polar](https://polar.sh) is a developer first payment infrastructure. Out of the box it provides a lot of developer first integrations for payments, checkouts and more. This plugin helps you integrate Polar with Better Auth to make your auth + payments flow seamless.
This plugin is maintained by Polar team. For bugs, issues or feature requests,
please visit the [Polar GitHub
repo](https://github.com/polarsource/polar-adapters).
## Features
* Checkout Integration
* Customer Portal
* Automatic Customer creation on signup
* Event Ingestion & Customer Meters for flexible Usage Based Billing
* Handle Polar Webhooks securely with signature verification
* Reference System to associate purchases with organizations
## Installation
```bash
pnpm add better-auth @polar-sh/better-auth @polar-sh/sdk
```
## Preparation
Go to your Polar Organization Settings, and create an Organization Access Token. Add it to your environment.
```bash
# .env
POLAR_ACCESS_TOKEN=...
```
### Configuring BetterAuth Server
The Polar plugin comes with a handful additional plugins which adds functionality to your stack.
* Checkout - Enables a seamless checkout integration
* Portal - Makes it possible for your customers to manage their orders, subscriptions & granted benefits
* Usage - Simple extension for listing customer meters & ingesting events for Usage Based Billing
* Webhooks - Listen for relevant Polar webhooks
```typescript
import { betterAuth } from "better-auth";
import { polar, checkout, portal, usage, webhooks } from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";
const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
// Use 'sandbox' if you're using the Polar Sandbox environment
// Remember that access tokens, products, etc. are completely separated between environments.
// Access tokens obtained in Production are for instance not usable in the Sandbox environment.
server: 'sandbox'
});
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
use: [
checkout({
products: [
{
productId: "123-456-789", // ID of Product from Polar Dashboard
slug: "pro" // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro
}
],
successUrl: "/success?checkout_id={CHECKOUT_ID}",
authenticatedUsersOnly: true
}),
portal(),
usage(),
webhooks({
secret: process.env.POLAR_WEBHOOK_SECRET,
onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
... // Over 25 granular webhook handlers
onPayload: (payload) => // Catch-all for all events
})
],
})
]
});
```
### Configuring BetterAuth Client
You will be using the BetterAuth Client to interact with the Polar functionalities.
```typescript
import { createAuthClient } from "better-auth/react";
import { polarClient } from "@polar-sh/better-auth";
// This is all that is needed
// All Polar plugins, etc. should be attached to the server-side BetterAuth config
export const authClient = createAuthClient({
plugins: [polarClient()],
});
```
## Configuration Options
```typescript
import { betterAuth } from "better-auth";
import {
polar,
checkout,
portal,
usage,
webhooks,
} from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";
const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
// Use 'sandbox' if you're using the Polar Sandbox environment
// Remember that access tokens, products, etc. are completely separated between environments.
// Access tokens obtained in Production are for instance not usable in the Sandbox environment.
server: "sandbox",
});
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
getCustomerCreateParams: ({ user }, request) => ({
metadata: {
myCustomProperty: 123,
},
}),
use: [
// This is where you add Polar plugins
],
}),
],
});
```
### Required Options
* `client`: Polar SDK client instance
### Optional Options
* `createCustomerOnSignUp`: Automatically create a Polar customer when a user signs up
* `getCustomerCreateParams`: Custom function to provide additional customer creation metadata
### Customers
When `createCustomerOnSignUp` is enabled, a new Polar Customer is automatically created when a new User is added in the Better-Auth Database.
All new customers are created with an associated `externalId`, which is the ID of your User in the Database. This allows us to skip any Polar to User mapping in your Database.
## Checkout Plugin
To support checkouts in your app, simply pass the Checkout plugin to the use-property.
```typescript
import { polar, checkout } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
checkout({
// Optional field - will make it possible to pass a slug to checkout instead of Product ID
products: [ { productId: "123-456-789", slug: "pro" } ],
// Relative URL to return to when checkout is successfully completed
successUrl: "/success?checkout_id={CHECKOUT_ID}",
// Wheather you want to allow unauthenticated checkout sessions or not
authenticatedUsersOnly: true
})
],
})
]
});
```
When checkouts are enabled, you're able to initialize Checkout Sessions using the checkout-method on the BetterAuth Client. This will redirect the user to the Product Checkout.
```typescript
await authClient.checkout({
// Any Polar Product ID can be passed here
products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
// Or, if you setup "products" in the Checkout Config, you can pass the slug
slug: "pro",
});
```
Checkouts will automatically carry the authenticated User as the customer to the checkout. Email-address will be "locked-in".
If `authenticatedUsersOnly` is `false` - then it will be possible to trigger checkout sessions without any associated customer.
### Organization Support
This plugin supports the Organization plugin. If you pass the organization ID to the Checkout referenceId, you will be able to keep track of purchases made from organization members.
```typescript
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,
await authClient.checkout({
// Any Polar Product ID can be passed here
products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
// Or, if you setup "products" in the Checkout Config, you can pass the slug
slug: 'pro',
// Reference ID will be saved as `referenceId` in the metadata of the checkout, order & subscription object
referenceId: organizationId
});
```
## Portal Plugin
A plugin which enables customer management of their purchases, orders and subscriptions.
```typescript
import { polar, checkout, portal } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
checkout(...),
portal()
],
})
]
});
```
The portal-plugin gives the BetterAuth Client a set of customer management methods, scoped under `authClient.customer`.
### Customer Portal Management
The following method will redirect the user to the Polar Customer Portal, where they can see orders, purchases, subscriptions, benefits, etc.
```typescript
await authClient.customer.portal();
```
### Customer State
The portal plugin also adds a convenient state-method for retrieving the general Customer State.
```typescript
const { data: customerState } = await authClient.customer.state();
```
The customer state object contains:
* All the data about the customer.
* The list of their active subscriptions
* Note: This does not include subscriptions done by a parent organization. See the subscription list-method below for more information.
* The list of their granted benefits.
* The list of their active meters, with their current balance.
Thus, with that single object, you have all the required information to check if you should provision access to your service or not.
[You can learn more about the Polar Customer State in the Polar Docs](https://docs.polar.sh/integrate/customer-state).
### Benefits, Orders & Subscriptions
The portal plugin adds 3 convenient methods for listing benefits, orders & subscriptions relevant to the authenticated user/customer.
[All of these methods use the Polar CustomerPortal APIs](https://docs.polar.sh/api-reference/customer-portal)
#### Benefits
This method only lists granted benefits for the authenticated user/customer.
```typescript
const { data: benefits } = await authClient.customer.benefits.list({
query: {
page: 1,
limit: 10,
},
});
```
#### Orders
This method lists orders like purchases and subscription renewals for the authenticated user/customer.
```typescript
const { data: orders } = await authClient.customer.orders.list({
query: {
page: 1,
limit: 10,
productBillingType: "one_time", // or 'recurring'
},
});
```
#### Subscriptions
This method lists the subscriptions associated with authenticated user/customer.
```typescript
const { data: subscriptions } = await authClient.customer.subscriptions.list({
query: {
page: 1,
limit: 10,
active: true,
},
});
```
**Important** - Organization Support
This will **not** return subscriptions made by a parent organization to the authenticated user.
However, you can pass a `referenceId` to this method. This will return all subscriptions associated with that referenceId instead of subscriptions associated with the user.
So in order to figure out if a user should have access, pass the user's organization ID to see if there is an active subscription for that organization.
```typescript
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,
const { data: subscriptions } = await authClient.customer.orders.list({
query: {
page: 1,
limit: 10,
active: true,
referenceId: organizationId
},
});
const userShouldHaveAccess = subscriptions.some(
sub => // Your logic to check subscription product or whatever.
)
```
## Usage Plugin
A simple plugin for Usage Based Billing.
```typescript
import { polar, checkout, portal, usage } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
checkout(...),
portal(),
usage()
],
})
]
});
```
### Event Ingestion
Polar's Usage Based Billing builds entirely on event ingestion. Ingest events from your application, create Meters to represent that usage, and add metered prices to Products to charge for it.
[Learn more about Usage Based Billing in the Polar Docs.](https://docs.polar.sh/features/usage-based-billing/introduction)
```typescript
const { data: ingested } = await authClient.usage.ingest({
event: "file-uploads",
metadata: {
uploadedFiles: 12,
},
});
```
The authenticated user is automatically associated with the ingested event.
### Customer Meters
A simple method for listing the authenticated user's Usage Meters, or as we call them, Customer Meters.
Customer Meter's contains all information about their consumtion on your defined meters.
* Customer Information
* Meter Information
* Customer Meter Information
* Consumed Units
* Credited Units
* Balance
```typescript
const { data: customerMeters } = await authClient.usage.meters.list({
query: {
page: 1,
limit: 10,
},
});
```
## Webhooks Plugin
The Webhooks plugin can be used to capture incoming events from your Polar organization.
```typescript
import { polar, webhooks } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
webhooks({
secret: process.env.POLAR_WEBHOOK_SECRET,
onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
... // Over 25 granular webhook handlers
onPayload: (payload) => // Catch-all for all events
})
],
})
]
});
```
Configure a Webhook endpoint in your Polar Organization Settings page. Webhook endpoint is configured at /polar/webhooks.
Add the secret to your environment.
```bash
# .env
POLAR_WEBHOOK_SECRET=...
```
The plugin supports handlers for all Polar webhook events:
* `onPayload` - Catch-all handler for any incoming Webhook event
* `onCheckoutCreated` - Triggered when a checkout is created
* `onCheckoutUpdated` - Triggered when a checkout is updated
* `onOrderCreated` - Triggered when an order is created
* `onOrderPaid` - Triggered when an order is paid
* `onOrderRefunded` - Triggered when an order is refunded
* `onRefundCreated` - Triggered when a refund is created
* `onRefundUpdated` - Triggered when a refund is updated
* `onSubscriptionCreated` - Triggered when a subscription is created
* `onSubscriptionUpdated` - Triggered when a subscription is updated
* `onSubscriptionActive` - Triggered when a subscription becomes active
* `onSubscriptionCanceled` - Triggered when a subscription is canceled
* `onSubscriptionRevoked` - Triggered when a subscription is revoked
* `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed
* `onProductCreated` - Triggered when a product is created
* `onProductUpdated` - Triggered when a product is updated
* `onOrganizationUpdated` - Triggered when an organization is updated
* `onBenefitCreated` - Triggered when a benefit is created
* `onBenefitUpdated` - Triggered when a benefit is updated
* `onBenefitGrantCreated` - Triggered when a benefit grant is created
* `onBenefitGrantUpdated` - Triggered when a benefit grant is updated
* `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked
* `onCustomerCreated` - Triggered when a customer is created
* `onCustomerUpdated` - Triggered when a customer is updated
* `onCustomerDeleted` - Triggered when a customer is deleted
* `onCustomerStateChanged` - Triggered when a customer is created
# plugins: Sign In With Ethereum (SIWE)
URL: /docs/plugins/siwe
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/siwe.mdx
Sign in with Ethereum plugin for Better Auth
***
title: Sign In With Ethereum (SIWE)
description: Sign in with Ethereum plugin for Better Auth
---------------------------------------------------------
The Sign in with Ethereum (SIWE) plugin allows users to authenticate using their Ethereum wallets following the [ERC-4361 standard](https://eips.ethereum.org/EIPS/eip-4361). This plugin provides flexibility by allowing you to implement your own message verification and nonce generation logic.
## Installation
### Add the Server Plugin
Add the SIWE plugin to your auth configuration:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
siwe({
domain: "example.com",
emailDomainName: "example.com", // optional
anonymous: false, // optional, default is true
getNonce: async () => {
// Implement your nonce generation logic here
return "your-secure-random-nonce";
},
verifyMessage: async (args) => {
// Implement your SIWE message verification logic here
// This should verify the signature against the message
return true; // return true if signature is valid
},
ensLookup: async (args) => {
// Optional: Implement ENS lookup for user names and avatars
return {
name: "user.eth",
avatar: "https://example.com/avatar.png"
};
},
}),
],
});
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the Client Plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [siweClient()],
});
```
## Usage
### Generate a Nonce
Before signing a SIWE message, you need to generate a nonce for the wallet address:
```ts title="generate-nonce.ts"
const { data, error } = await authClient.siwe.nonce({
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // optional for Ethereum mainnet, required for other chains. Defaults to 1
});
if (data) {
console.log("Nonce:", data.nonce);
}
```
### Sign In with Ethereum
After generating a nonce and creating a SIWE message, verify the signature to authenticate:
```ts title="sign-in-siwe.ts"
const { data, error } = await authClient.siwe.verify({
message: "Your SIWE message string",
signature: "0x...", // The signature from the user's wallet
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // optional for Ethereum mainnet, required for other chains. Must match Chain ID in SIWE message
email: "user@example.com", // optional, required if anonymous is false
});
if (data) {
console.log("Authentication successful:", data.user);
}
```
### Chain-Specific Examples
Here are examples for different blockchain networks:
```ts title="ethereum-mainnet.ts"
// Ethereum Mainnet (chainId can be omitted, defaults to 1)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
// chainId: 1 (default)
});
```
```ts title="polygon.ts"
// Polygon (chainId REQUIRED)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 137, // Required for Polygon
});
```
```ts title="arbitrum.ts"
// Arbitrum (chainId REQUIRED)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 42161, // Required for Arbitrum
});
```
```ts title="base.ts"
// Base (chainId REQUIRED)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 8453, // Required for Base
});
```
The `chainId` must match the Chain ID specified in your SIWE message. Verification will fail with a 401 error if there's a mismatch between the message's Chain ID and the `chainId` parameter.
## Configuration Options
### Server Options
The SIWE plugin accepts the following configuration options:
* **domain**: The domain name of your application (required for SIWE message generation)
* **emailDomainName**: The email domain name for creating user accounts when not using anonymous mode. Defaults to the domain from your base URL
* **anonymous**: Whether to allow anonymous sign-ins without requiring an email. Default is `true`
* **getNonce**: Function to generate a unique nonce for each sign-in attempt. You must implement this function to return a cryptographically secure random string. Must return a `Promise`
* **verifyMessage**: Function to verify the signed SIWE message. Receives message details and should return `Promise`
* **ensLookup**: Optional function to lookup ENS names and avatars for Ethereum addresses
### Client Options
The SIWE client plugin doesn't require any configuration options, but you can pass them if needed for future extensibility:
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
siweClient({
// Optional client configuration can go here
}),
],
});
```
## Schema
The SIWE plugin adds a `walletAddress` table to store user wallet associations:
| Field | Type | Description |
| --------- | ------- | ----------------------------------------- |
| id | string | Primary key |
| userId | string | Reference to user.id |
| address | string | Ethereum wallet address |
| chainId | number | Chain ID (e.g., 1 for Ethereum mainnet) |
| isPrimary | boolean | Whether this is the user's primary wallet |
| createdAt | date | Creation timestamp |
## Example Implementation
Here's a complete example showing how to implement SIWE authentication:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
import { generateRandomString } from "better-auth/crypto";
import { verifyMessage, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
export const auth = betterAuth({
database: {
// your database configuration
},
plugins: [
siwe({
domain: "myapp.com",
emailDomainName: "myapp.com",
anonymous: false,
getNonce: async () => {
// Generate a cryptographically secure random nonce
return generateRandomString(32);
},
verifyMessage: async ({ message, signature, address }) => {
try {
// Verify the signature using viem (recommended)
const isValid = await verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`,
});
return isValid;
} catch (error) {
console.error("SIWE verification failed:", error);
return false;
}
},
ensLookup: async ({ walletAddress }) => {
try {
// Optional: lookup ENS name and avatar using viem
// You can use viem's ENS utilities here
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const ensName = await client.getEnsName({
address: walletAddress as `0x${string}`,
});
const ensAvatar = ensName
? await client.getEnsAvatar({
name: ensName,
})
: null;
return {
name: ensName || walletAddress,
avatar: ensAvatar || "",
};
} catch {
return {
name: walletAddress,
avatar: "",
};
}
},
}),
],
});
```
# plugins: Single Sign-On (SSO)
URL: /docs/plugins/sso
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/sso.mdx
Integrate Single Sign-On (SSO) with your application.
***
title: Single Sign-On (SSO)
description: Integrate Single Sign-On (SSO) with your application.
------------------------------------------------------------------
`OIDC` `OAuth2` `SSO` `SAML`
Single Sign-On (SSO) allows users to authenticate with multiple applications using a single set of credentials. This plugin supports OpenID Connect (OIDC), OAuth2 providers, and SAML 2.0.
This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on [GitHub](https://github.com/better-auth/better-auth) and any security concerns on [security@better-auth.com](mailto:security@better-auth.com).
## Installation
### Install the plugin
```bash
npm install @better-auth/sso
```
### Add Plugin to the server
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { sso } from "@better-auth/sso";
const auth = betterAuth({
plugins: [ // [!code highlight]
sso() // [!code highlight]
] // [!code highlight]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { ssoClient } from "@better-auth/sso/client"
const authClient = createAuthClient({
plugins: [ // [!code highlight]
ssoClient() // [!code highlight]
] // [!code highlight]
})
```
## Usage
### Register an OIDC Provider
To register an OIDC provider, use the `registerSSOProvider` endpoint and provide the necessary configuration details for the provider.
A redirect URL will be automatically generated using the provider ID. For instance, if the provider ID is `hydra`, the redirect URL would be `{baseURL}/api/auth/sso/callback/hydra`. Note that `/api/auth` may vary depending on your base path configuration.
#### Example
```ts title="register-oidc-provider.ts"
import { authClient } from "@/lib/auth-client";
// Register with OIDC configuration
await authClient.sso.register({
providerId: "example-provider",
issuer: "https://idp.example.com",
domain: "example.com",
oidcConfig: {
clientId: "client-id",
clientSecret: "client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true,
},
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
},
});
```
```ts title="register-oidc-provider.ts"
const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
providerId: "example-provider",
issuer: "https://idp.example.com",
domain: "example.com",
oidcConfig: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true,
},
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
},
},
headers,
});
```
### Register a SAML Provider
To register a SAML provider, use the `registerSSOProvider` endpoint with SAML configuration details. The provider will act as a Service Provider (SP) and integrate with your Identity Provider (IdP).
```ts title="register-saml-provider.ts"
import { authClient } from "@/lib/auth-client";
await authClient.sso.register({
providerId: "saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
audience: "https://yourapp.com",
wantAssertionsSigned: true,
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
idpMetadata: {
metadata: "",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-encryption-key-password"
},
spMetadata: {
metadata: "",
binding: "post",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-sp-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-sp-encryption-key-password"
}
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
extraFields: {
department: "department",
role: "role"
}
},
});
```
```ts title="register-saml-provider.ts"
const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
providerId: "saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
audience: "https://yourapp.com",
wantAssertionsSigned: true,
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
idpMetadata: {
metadata: "",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-encryption-key-password"
},
spMetadata: {
metadata: "",
binding: "post",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-sp-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-sp-encryption-key-password"
}
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
extraFields: {
department: "department",
role: "role"
}
},
},
headers,
});
```
### Get Service Provider Metadata
For SAML providers, you can retrieve the Service Provider metadata XML that needs to be configured in your Identity Provider:
```ts title="get-sp-metadata.ts"
const response = await auth.api.spMetadata({
query: {
providerId: "saml-provider",
format: "xml" // or "json"
}
});
const metadataXML = await response.text();
console.log(metadataXML);
```
### Sign In with SSO
To sign in with an SSO provider, you can call `signIn.sso`
You can sign in using the email with domain matching:
```ts title="sign-in.ts"
const res = await authClient.signIn.sso({
email: "user@example.com",
callbackURL: "/dashboard",
});
```
or you can specify the domain:
```ts title="sign-in-domain.ts"
const res = await authClient.signIn.sso({
domain: "example.com",
callbackURL: "/dashboard",
});
```
You can also sign in using the organization slug if a provider is associated with an organization:
```ts title="sign-in-org.ts"
const res = await authClient.signIn.sso({
organizationSlug: "example-org",
callbackURL: "/dashboard",
});
```
Alternatively, you can sign in using the provider's ID:
```ts title="sign-in-provider-id.ts"
const res = await authClient.signIn.sso({
providerId: "example-provider-id",
callbackURL: "/dashboard",
});
```
To use the server API you can use `signInSSO`
```ts title="sign-in-org.ts"
const res = await auth.api.signInSSO({
body: {
organizationSlug: "example-org",
callbackURL: "/dashboard",
}
});
```
#### Full method
### Client Side
```ts
const { data, error } = await authClient.signIn.sso({
email: john@example.com, // required
organizationSlug: example-org, // required
providerId: example-provider, // required
domain: example.com, // required
callbackURL: https://example.com/callback,
errorCallbackURL: https://example.com/callback, // required
newUserCallbackURL: https://example.com/new-user, // required
scopes, // required
requestSignUp, // required
});
```
### Server Side
```ts
const data = await auth.api.signInSSO({
body: {
email: john@example.com, // required
organizationSlug: example-org, // required
providerId: example-provider, // required
domain: example.com, // required
callbackURL: https://example.com/callback,
errorCallbackURL: https://example.com/callback, // required
newUserCallbackURL: https://example.com/new-user, // required
scopes, // required
requestSignUp, // required
}
});
```
### Type Definition
```ts
type signInSSO = {
/**
* The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided.
*/
email?: string = "john@example.com"
/**
* The slug of the organization to sign in with.
*/
organizationSlug?: string = "example-org"
/**
* The ID of the provider to sign in with. This can be provided instead of email or issuer.
*/
providerId?: string = "example-provider"
/**
* The domain of the provider.
*/
domain?: string = "example.com"
/**
* The URL to redirect to after login.
*/
callbackURL: string = "https://example.com/callback"
/**
* The URL to redirect to after login.
*/
errorCallbackURL?: string = "https://example.com/callback"
/**
* The URL to redirect to after login if the user is new.
*/
newUserCallbackURL?: string = "https://example.com/new-user"
/**
* Scopes to request from the provider.
*/
scopes?: string[] = ["openid", "email", "profile", "offline_access"]
/**
* Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider.
*/
requestSignUp?: boolean = true
}
```
When a user is authenticated, if the user does not exist, the user will be provisioned using the `provisionUser` function. If the organization provisioning is enabled and a provider is associated with an organization, the user will be added to the organization.
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
provisionUser: async (user) => {
// provision user
},
organizationProvisioning: {
disabled: false,
defaultRole: "member",
getRole: async (user) => {
// get role if needed
},
},
}),
],
});
```
## Provisioning
The SSO plugin provides powerful provisioning capabilities to automatically set up users and manage their organization memberships when they sign in through SSO providers.
### User Provisioning
User provisioning allows you to run custom logic whenever a user signs in through an SSO provider. This is useful for:
* Setting up user profiles with additional data from the SSO provider
* Synchronizing user attributes with external systems
* Creating user-specific resources
* Logging SSO sign-ins
* Updating user information from the SSO provider
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
provisionUser: async ({ user, userInfo, token, provider }) => {
// Update user profile with SSO data
await updateUserProfile(user.id, {
department: userInfo.attributes?.department,
jobTitle: userInfo.attributes?.jobTitle,
manager: userInfo.attributes?.manager,
lastSSOLogin: new Date(),
});
// Create user-specific resources
await createUserWorkspace(user.id);
// Sync with external systems
await syncUserWithCRM(user.id, userInfo);
// Log the SSO sign-in
await auditLog.create({
userId: user.id,
action: 'sso_signin',
provider: provider.providerId,
metadata: {
email: userInfo.email,
ssoProvider: provider.issuer,
},
});
},
}),
],
});
```
The `provisionUser` function receives:
* **user**: The user object from the database
* **userInfo**: User information from the SSO provider (includes attributes, email, name, etc.)
* **token**: OAuth2 tokens (for OIDC providers) - may be undefined for SAML
* **provider**: The SSO provider configuration
### Organization Provisioning
Organization provisioning automatically manages user memberships in organizations when SSO providers are linked to specific organizations. This is particularly useful for:
* Enterprise SSO where each company/domain maps to an organization
* Automatic role assignment based on SSO attributes
* Managing team memberships through SSO
#### Basic Organization Provisioning
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
organizationProvisioning: {
disabled: false, // Enable org provisioning
defaultRole: "member", // Default role for new members
},
}),
],
});
```
#### Advanced Organization Provisioning with Custom Roles
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
organizationProvisioning: {
disabled: false,
defaultRole: "member",
getRole: async ({ user, userInfo, provider }) => {
// Assign roles based on SSO attributes
const department = userInfo.attributes?.department;
const jobTitle = userInfo.attributes?.jobTitle;
// Admins based on job title
if (jobTitle?.toLowerCase().includes('manager') ||
jobTitle?.toLowerCase().includes('director') ||
jobTitle?.toLowerCase().includes('vp')) {
return "admin";
}
// Special roles for IT department
if (department?.toLowerCase() === 'it') {
return "admin";
}
// Default to member for everyone else
return "member";
},
},
}),
],
});
```
#### Linking SSO Providers to Organizations
When registering an SSO provider, you can link it to a specific organization:
```ts title="register-org-provider.ts"
await auth.api.registerSSOProvider({
body: {
providerId: "acme-corp-saml",
issuer: "https://acme-corp.okta.com",
domain: "acmecorp.com",
organizationId: "org_acme_corp_id", // Link to organization
samlConfig: {
// SAML configuration...
},
},
headers,
});
```
Now when users from `acmecorp.com` sign in through this provider, they'll automatically be added to the "Acme Corp" organization with the appropriate role.
#### Multiple Organizations Example
You can set up multiple SSO providers for different organizations:
```ts title="multi-org-setup.ts"
// Acme Corp SAML provider
await auth.api.registerSSOProvider({
body: {
providerId: "acme-corp",
issuer: "https://acme.okta.com",
domain: "acmecorp.com",
organizationId: "org_acme_id",
samlConfig: { /* ... */ },
},
headers,
});
// TechStart OIDC provider
await auth.api.registerSSOProvider({
body: {
providerId: "techstart-google",
issuer: "https://accounts.google.com",
domain: "techstart.io",
organizationId: "org_techstart_id",
oidcConfig: { /* ... */ },
},
headers,
});
```
#### Organization Provisioning Flow
1. **User signs in** through an SSO provider linked to an organization
2. **User is authenticated** and either found or created in the database
3. **Organization membership is checked** - if the user isn't already a member of the linked organization
4. **Role is determined** using either the `defaultRole` or `getRole` function
5. **User is added** to the organization with the determined role
6. **User provisioning runs** (if configured) for additional setup
### Provisioning Best Practices
#### 1. Idempotent Operations
Make sure your provisioning functions can be safely run multiple times:
```ts
provisionUser: async ({ user, userInfo }) => {
// Check if already provisioned
const existingProfile = await getUserProfile(user.id);
if (!existingProfile.ssoProvisioned) {
await createUserResources(user.id);
await markAsProvisioned(user.id);
}
// Always update attributes (they might change)
await updateUserAttributes(user.id, userInfo.attributes);
},
```
#### 2. Error Handling
Handle errors gracefully to avoid blocking user sign-in:
```ts
provisionUser: async ({ user, userInfo }) => {
try {
await syncWithExternalSystem(user, userInfo);
} catch (error) {
// Log error but don't throw - user can still sign in
console.error('Failed to sync user with external system:', error);
await logProvisioningError(user.id, error);
}
},
```
#### 3. Conditional Provisioning
Only run certain provisioning steps when needed:
```ts
organizationProvisioning: {
disabled: false,
getRole: async ({ user, userInfo, provider }) => {
// Only process role assignment for certain providers
if (provider.providerId.includes('enterprise')) {
return determineEnterpriseRole(userInfo);
}
return "member";
},
},
```
## SAML Configuration
### Service Provider Configuration
When registering a SAML provider, you need to provide Service Provider (SP) metadata configuration:
* **metadata**: XML metadata for the Service Provider
* **binding**: The binding method, typically "post" or "redirect"
* **privateKey**: Private key for signing (optional)
* **privateKeyPass**: Password for the private key (if encrypted)
* **isAssertionEncrypted**: Whether assertions should be encrypted
* **encPrivateKey**: Private key for decryption (if encryption is enabled)
* **encPrivateKeyPass**: Password for the encryption private key
### Identity Provider Configuration
You also need to provide Identity Provider (IdP) configuration:
* **metadata**: XML metadata from your Identity Provider
* **privateKey**: Private key for the IdP communication (optional)
* **privateKeyPass**: Password for the IdP private key (if encrypted)
* **isAssertionEncrypted**: Whether assertions from IdP are encrypted
* **encPrivateKey**: Private key for IdP assertion decryption
* **encPrivateKeyPass**: Password for the IdP decryption key
### SAML Attribute Mapping
Configure how SAML attributes map to user fields:
```ts
mapping: {
id: "nameID", // Default: "nameID"
email: "email", // Default: "email" or "nameID"
name: "displayName", // Default: "displayName"
firstName: "givenName", // Default: "givenName"
lastName: "surname", // Default: "surname"
extraFields: {
department: "department",
role: "jobTitle",
phone: "telephoneNumber"
}
}
```
### SAML Endpoints
The plugin automatically creates the following SAML endpoints:
* **SP Metadata**: `/api/auth/sso/saml2/sp/metadata?providerId={providerId}`
* **SAML Callback**: `/api/auth/sso/saml2/callback/{providerId}`
## Schema
The plugin requires additional fields in the `ssoProvider` table to store the provider's configuration.
## Options
### Server
**provisionUser**: A custom function to provision a user when they sign in with an SSO provider.
**organizationProvisioning**: Options for provisioning users to an organization.
**defaultOverrideUserInfo**: Override user info with the provider info by default.
**disableImplicitSignUp**: Disable implicit sign up for new users.
**trustEmailVerified**: Trust the email verified flag from the provider.
# plugins: Stripe
URL: /docs/plugins/stripe
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/stripe.mdx
Stripe plugin for Better Auth to manage subscriptions and payments.
***
title: Stripe
description: Stripe plugin for Better Auth to manage subscriptions and payments.
--------------------------------------------------------------------------------
The Stripe plugin integrates Stripe's payment and subscription functionality with Better Auth. Since payment and authentication are often tightly coupled, this plugin simplifies the integration of Stripe into your application, handling customer creation, subscription management, and webhook processing.
## Features
* Create Stripe Customers automatically when users sign up
* Manage subscription plans and pricing
* Process subscription lifecycle events (creation, updates, cancellations)
* Handle Stripe webhooks securely with signature verification
* Expose subscription data to your application
* Support for trial periods and subscription upgrades
* **Automatic trial abuse prevention** - Users can only get one trial per account across all plans
* Flexible reference system to associate subscriptions with users or organizations
* Team subscription support with seats management
## Installation
### Install the plugin
First, install the plugin:
npm
pnpm
yarn
bun
```bash
npm install @better-auth/stripe
```
```bash
pnpm add @better-auth/stripe
```
```bash
yarn add @better-auth/stripe
```
```bash
bun add @better-auth/stripe
```
If you're using a separate client and server setup, make sure to install the plugin in both parts of your project.
### Install the Stripe SDK
Next, install the Stripe SDK on your server:
npm
pnpm
yarn
bun
```bash
npm install stripe@^18.0.0
```
```bash
pnpm add stripe@^18.0.0
```
```bash
yarn add stripe@^18.0.0
```
```bash
bun add stripe@^18.0.0
```
### Add the plugin to your auth config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { stripe } from "@better-auth/stripe"
import Stripe from "stripe"
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-02-24.acacia",
})
export const auth = betterAuth({
// ... your existing config
plugins: [
stripe({
stripeClient,
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
createCustomerOnSignUp: true,
})
]
})
```
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"
export const client = createAuthClient({
// ... your existing config
plugins: [
stripeClient({
subscription: true //if you want to enable subscription management
})
]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the tables manually.
### Set up Stripe webhooks
Create a webhook endpoint in your Stripe dashboard pointing to:
```
https://your-domain.com/api/auth/stripe/webhook
```
`/api/auth` is the default path for the auth server.
Make sure to select at least these events:
* `checkout.session.completed`
* `customer.subscription.updated`
* `customer.subscription.deleted`
Save the webhook signing secret provided by Stripe and add it to your environment variables as `STRIPE_WEBHOOK_SECRET`.
## Usage
### Customer Management
You can use this plugin solely for customer management without enabling subscriptions. This is useful if you just want to link Stripe customers to your users.
By default, when a user signs up, a Stripe customer is automatically created if you set `createCustomerOnSignUp: true`. This customer is linked to the user in your database.
You can customize the customer creation process:
```ts title="auth.ts"
stripe({
// ... other options
createCustomerOnSignUp: true,
onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
// Do something with the newly created customer
console.log(`Customer ${customer.id} created for user ${user.id}`);
},
getCustomerCreateParams: async ({ user, session }, request) => {
// Customize the Stripe customer creation parameters
return {
metadata: {
referralSource: user.metadata?.referralSource
}
};
}
})
```
### Subscription Management
#### Defining Plans
You can define your subscription plans either statically or dynamically:
```ts title="auth.ts"
// Static plans
subscription: {
enabled: true,
plans: [
{
name: "basic", // the name of the plan, it'll be automatically lower cased when stored in the database
priceId: "price_1234567890", // the price ID from stripe
annualDiscountPriceId: "price_1234567890", // (optional) the price ID for annual billing with a discount
limits: {
projects: 5,
storage: 10
}
},
{
name: "pro",
priceId: "price_0987654321",
limits: {
projects: 20,
storage: 50
},
freeTrial: {
days: 14,
}
}
]
}
// Dynamic plans (fetched from database or API)
subscription: {
enabled: true,
plans: async () => {
const plans = await db.query("SELECT * FROM plans");
return plans.map(plan => ({
name: plan.name,
priceId: plan.stripe_price_id,
limits: JSON.parse(plan.limits)
}));
}
}
```
see [plan configuration](#plan-configuration) for more.
#### Creating a Subscription
To create a subscription, use the `subscription.upgrade` method:
### Client Side
```ts
const { data, error } = await authClient.subscription.upgrade({
plan: pro,
annual, // required
referenceId: 123, // required
subscriptionId: sub_123, // required
metadata, // required
seats, // required
successUrl,
cancelUrl,
returnUrl, // required
disableRedirect,
});
```
### Server Side
```ts
const data = await auth.api.upgradeSubscription({
body: {
plan: pro,
annual, // required
referenceId: 123, // required
subscriptionId: sub_123, // required
metadata, // required
seats, // required
successUrl,
cancelUrl,
returnUrl, // required
disableRedirect,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type upgradeSubscription = {
/**
* The name of the plan to upgrade to.
*/
plan: string = "pro"
/**
* Whether to upgrade to an annual plan.
*/
annual?: boolean = true
/**
* Reference id of the subscription to upgrade.
*/
referenceId?: string = "123"
/**
* The id of the subscription to upgrade.
*/
subscriptionId?: string = "sub_123"
metadata?: Record
/**
* Number of seats to upgrade to (if applicable).
*/
seats?: number = 1
/**
* Callback URL to redirect back after successful subscription.
*/
successUrl: string
/**
* If set, checkout shows a back button and customers will be directed here if they cancel payment.
*/
cancelUrl: string
/**
* URL to take customers to when they click on the billing portal’s link to return to your website.
*/
returnUrl?: string
/**
* Disable redirect after successful subscription.
*/
disableRedirect: boolean = true
}
```
**Simple Example:**
```ts title="client.ts"
await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
annual: true, // Optional: upgrade to an annual plan
referenceId: "org_123", // Optional: defaults to the current logged in user ID
seats: 5 // Optional: for team plans
});
```
This will create a Checkout Session and redirect the user to the Stripe Checkout page.
If the user already has an active subscription, you *must* provide the `subscriptionId` parameter. Otherwise, the user will be subscribed to (and pay for) both plans.
> **Important:** The `successUrl` parameter will be internally modified to handle race conditions between checkout completion and webhook processing. The plugin creates an intermediate redirect that ensures subscription status is properly updated before redirecting to your success page.
```ts
const { error } = await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
if(error) {
alert(error.message);
}
```
For each reference ID (user or organization), only one active or trialing subscription is supported at a time. The plugin doesn't currently support multiple concurrent active subscriptions for the same reference ID.
#### Switching Plans
To switch a subscription to a different plan, use the `subscription.upgrade` method:
```ts title="client.ts"
await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
subscriptionId: "sub_123", // the Stripe subscription ID of the user's current plan
});
```
This ensures that the user only pays for the new plan, and not both.
#### Listing Active Subscriptions
To get the user's active subscriptions:
### Client Side
```ts
const { data, error } = await authClient.subscription.list({
referenceId: 123, // required
});
```
### Server Side
```ts
const subscriptions = await auth.api.listActiveSubscriptions({
query: {
referenceId: 123, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type listActiveSubscriptions = {
/**
* Reference id of the subscription to list.
*/
referenceId?: string = '123'
}
```
#### Canceling a Subscription
To cancel a subscription:
### Client Side
```ts
const { data, error } = await authClient.subscription.cancel({
referenceId: org_123, // required
subscriptionId: sub_123, // required
returnUrl: /account,
});
```
### Server Side
```ts
const data = await auth.api.cancelSubscription({
body: {
referenceId: org_123, // required
subscriptionId: sub_123, // required
returnUrl: /account,
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type cancelSubscription = {
/**
* Reference id of the subscription to cancel. Defaults to the userId.
*/
referenceId?: string = 'org_123'
/**
* The id of the subscription to cancel.
*/
subscriptionId?: string = 'sub_123'
/**
* URL to take customers to when they click on the billing portal’s link to return to your website.
*/
returnUrl: string = '/account'
}
```
This will redirect the user to the Stripe Billing Portal where they can cancel their subscription.
#### Restoring a Canceled Subscription
If a user changes their mind after canceling a subscription (but before the subscription period ends), you can restore the subscription:
### Client Side
```ts
const { data, error } = await authClient.subscription.restore({
referenceId: 123, // required
subscriptionId: sub_123, // required
});
```
### Server Side
```ts
const data = await auth.api.restoreSubscription({
body: {
referenceId: 123, // required
subscriptionId: sub_123, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type restoreSubscription = {
/**
* Reference id of the subscription to restore. Defaults to the userId.
*/
referenceId?: string = '123'
/**
* The id of the subscription to restore.
*/
subscriptionId?: string = 'sub_123'
}
```
This will reactivate a subscription that was previously set to cancel at the end of the billing period (`cancelAtPeriodEnd: true`). The subscription will continue to renew automatically.
> **Note:** This only works for subscriptions that are still active but marked to cancel at the end of the period. It cannot restore subscriptions that have already ended.
#### Creating Billing Portal Sessions
To create a [Stripe billing portal session](https://docs.stripe.com/api/customer_portal/sessions/create) where customers can manage their subscriptions, update payment methods, and view billing history:
### Client Side
```ts
const { data, error } = await authClient.subscription.billingPortal({
locale, // required
referenceId: 123, // required
returnUrl, // required
});
```
### Server Side
```ts
const data = await auth.api.createBillingPortal({
body: {
locale, // required
referenceId: 123, // required
returnUrl, // required
},
// This endpoint requires session cookies.
headers: await headers()
});
```
### Type Definition
```ts
type createBillingPortal = {
/**
* The IETF language tag of the locale customer portal is displayed in. If blank or auto, browser's locale is used.
*/
locale?: string
/**
* Reference id of the subscription to upgrade.
*/
referenceId?: string = "123"
/**
* Return URL to redirect back after successful subscription.
*/
returnUrl?: string
}
```
For supported locales, see the [IETF language tag documentation](https://docs.stripe.com/js/appendix/supported_locales).
This endpoint creates a Stripe billing portal session and returns a URL in the response as `data.url`. You can redirect users to this URL to allow them to manage their subscription, payment methods, and billing history.
### Reference System
By default, subscriptions are associated with the user ID. However, you can use a custom reference ID to associate subscriptions with other entities, such as organizations:
```ts title="client.ts"
// Create a subscription for an organization
await client.subscription.upgrade({
plan: "pro",
referenceId: "org_123456",
successUrl: "/dashboard",
cancelUrl: "/pricing",
seats: 5 // Number of seats for team plans
});
// List subscriptions for an organization
const { data: subscriptions } = await client.subscription.list({
query: {
referenceId: "org_123456"
}
});
```
#### Team Subscriptions with Seats
For team or organization plans, you can specify the number of seats:
```ts
await client.subscription.upgrade({
plan: "team",
referenceId: "org_123456",
seats: 10, // 10 team members
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});
```
The `seats` parameter is passed to Stripe as the quantity for the subscription item. You can use this value in your application logic to limit the number of members in a team or organization.
To authorize reference IDs, implement the `authorizeReference` function:
```ts title="auth.ts"
subscription: {
// ... other options
authorizeReference: async ({ user, session, referenceId, action }) => {
// Check if the user has permission to manage subscriptions for this reference
if (action === "upgrade-subscription" || action === "cancel-subscription" || action === "restore-subscription") {
const org = await db.member.findFirst({
where: {
organizationId: referenceId,
userId: user.id
}
});
return org?.role === "owner"
}
return true;
}
}
```
### Webhook Handling
The plugin automatically handles common webhook events:
* `checkout.session.completed`: Updates subscription status after checkout
* `customer.subscription.updated`: Updates subscription details when changed
* `customer.subscription.deleted`: Marks subscription as canceled
You can also handle custom events:
```ts title="auth.ts"
stripe({
// ... other options
onEvent: async (event) => {
// Handle any Stripe event
switch (event.type) {
case "invoice.paid":
// Handle paid invoice
break;
case "payment_intent.succeeded":
// Handle successful payment
break;
}
}
})
```
### Subscription Lifecycle Hooks
You can hook into various subscription lifecycle events:
```ts title="auth.ts"
subscription: {
// ... other options
onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
// Called when a subscription is successfully created
await sendWelcomeEmail(subscription.referenceId, plan.name);
},
onSubscriptionUpdate: async ({ event, subscription }) => {
// Called when a subscription is updated
console.log(`Subscription ${subscription.id} updated`);
},
onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
// Called when a subscription is canceled
await sendCancellationEmail(subscription.referenceId);
},
onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
// Called when a subscription is deleted
console.log(`Subscription ${subscription.id} deleted`);
}
}
```
### Trial Periods
You can configure trial periods for your plans:
```ts title="auth.ts"
{
name: "pro",
priceId: "price_0987654321",
freeTrial: {
days: 14,
onTrialStart: async (subscription) => {
// Called when a trial starts
await sendTrialStartEmail(subscription.referenceId);
},
onTrialEnd: async ({ subscription, user }, request) => {
// Called when a trial ends
await sendTrialEndEmail(user.email);
},
onTrialExpired: async (subscription) => {
// Called when a trial expires without conversion
await sendTrialExpiredEmail(subscription.referenceId);
}
}
}
```
## Schema
The Stripe plugin adds the following tables to your database:
### User
Table Name: `user`
### Subscription
Table Name: `subscription`
### Customizing the Schema
To change the schema table names or fields, you can pass a `schema` option to the Stripe plugin:
```ts title="auth.ts"
stripe({
// ... other options
schema: {
subscription: {
modelName: "stripeSubscriptions", // map the subscription table to stripeSubscriptions
fields: {
plan: "planName" // map the plan field to planName
}
}
}
})
```
## Options
### Main Options
**stripeClient**: `Stripe` - The Stripe client instance. Required.
**stripeWebhookSecret**: `string` - The webhook signing secret from Stripe. Required.
**createCustomerOnSignUp**: `boolean` - Whether to automatically create a Stripe customer when a user signs up. Default: `false`.
**onCustomerCreate**: `(data: { customer: Customer, stripeCustomer: Stripe.Customer, user: User }, request?: Request) => Promise` - A function called after a customer is created.
**getCustomerCreateParams**: `(data: { user: User, session: Session }, request?: Request) => Promise<{}>` - A function to customize the Stripe customer creation parameters.
**onEvent**: `(event: Stripe.Event) => Promise` - A function called for any Stripe webhook event.
### Subscription Options
**enabled**: `boolean` - Whether to enable subscription functionality. Required.
**plans**: `Plan[] | (() => Promise)` - An array of subscription plans or a function that returns plans. Required if subscriptions are enabled.
**requireEmailVerification**: `boolean` - Whether to require email verification before allowing subscription upgrades. Default: `false`.
**authorizeReference**: `(data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription"}, request?: Request) => Promise` - A function to authorize reference IDs.
### Plan Configuration
Each plan can have the following properties:
**name**: `string` - The name of the plan. Required.
**priceId**: `string` - The Stripe price ID. Required unless using `lookupKey`.
**lookupKey**: `string` - The Stripe price lookup key. Alternative to `priceId`.
**annualDiscountPriceId**: `string` - A price ID for annual billing.
**annualDiscountLookupKey**: `string` - The Stripe price lookup key for annual billing. Alternative to `annualDiscountPriceId`.
**limits**: `Record` - Limits associated with the plan (e.g., `{ projects: 10, storage: 5 }`).
**group**: `string` - A group name for the plan, useful for categorizing plans.
**freeTrial**: Object containing trial configuration:
* **days**: `number` - Number of trial days.
* **onTrialStart**: `(subscription: Subscription) => Promise` - Called when a trial starts.
* **onTrialEnd**: `(data: { subscription: Subscription, user: User }, request?: Request) => Promise` - Called when a trial ends.
* **onTrialExpired**: `(subscription: Subscription) => Promise` - Called when a trial expires without conversion.
## Advanced Usage
### Using with Organizations
The Stripe plugin works well with the organization plugin. You can associate subscriptions with organizations instead of individual users:
```ts title="client.ts"
// Get the active organization
const { data: activeOrg } = client.useActiveOrganization();
// Create a subscription for the organization
await client.subscription.upgrade({
plan: "team",
referenceId: activeOrg.id,
seats: 10,
annual: true, // upgrade to an annual plan (optional)
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});
```
Make sure to implement the `authorizeReference` function to verify that the user has permission to manage subscriptions for the organization:
```ts title="auth.ts"
authorizeReference: async ({ user, referenceId, action }) => {
const member = await db.members.findFirst({
where: {
userId: user.id,
organizationId: referenceId
}
});
return member?.role === "owner" || member?.role === "admin";
}
```
### Custom Checkout Session Parameters
You can customize the Stripe Checkout session with additional parameters:
```ts title="auth.ts"
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
return {
params: {
allow_promotion_codes: true,
tax_id_collection: {
enabled: true
},
billing_address_collection: "required",
custom_text: {
submit: {
message: "We'll start your subscription right away"
}
},
metadata: {
planType: "business",
referralCode: user.metadata?.referralCode
}
},
options: {
idempotencyKey: `sub_${user.id}_${plan.name}_${Date.now()}`
}
};
}
```
### Tax Collection
To collect tax IDs from the customer, set `tax_id_collection` to true:
```ts title="auth.ts"
subscription: {
// ... other options
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
return {
params: {
tax_id_collection: {
enabled: true
}
}
};
}
}
```
### Automatic Tax Calculation
To enable automatic tax calculation using the customer's location, set `automatic_tax` to true. Enabling this parameter causes Checkout to collect any billing address information necessary for tax calculation. You need to have tax registration setup and configured in the Stripe dashboard first for this to work.
```ts title="auth.ts"
subscription: {
// ... other options
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
return {
params: {
automatic_tax: {
enabled: true
}
}
};
}
}
```
### Trial Period Management
The Stripe plugin automatically prevents users from getting multiple free trials. Once a user has used a trial period (regardless of which plan), they will not be eligible for additional trials on any plan.
**How it works:**
* The system tracks trial usage across all plans for each user
* When a user subscribes to a plan with a trial, the system checks their subscription history
* If the user has ever had a trial (indicated by `trialStart`/`trialEnd` fields or `trialing` status), no new trial will be offered
* This prevents abuse where users cancel subscriptions and resubscribe to get multiple free trials
**Example scenario:**
1. User subscribes to "Starter" plan with 7-day trial
2. User cancels the subscription after the trial
3. User tries to subscribe to "Premium" plan - no trial will be offered
4. User will be charged immediately for the Premium plan
This behavior is automatic and requires no additional configuration. The trial eligibility is determined at the time of subscription creation and cannot be overridden through configuration.
## Troubleshooting
### Webhook Issues
If webhooks aren't being processed correctly:
1. Check that your webhook URL is correctly configured in the Stripe dashboard
2. Verify that the webhook signing secret is correct
3. Ensure you've selected all the necessary events in the Stripe dashboard
4. Check your server logs for any errors during webhook processing
### Subscription Status Issues
If subscription statuses aren't updating correctly:
1. Make sure the webhook events are being received and processed
2. Check that the `stripeCustomerId` and `stripeSubscriptionId` fields are correctly populated
3. Verify that the reference IDs match between your application and Stripe
### Testing Webhooks Locally
For local development, you can use the Stripe CLI to forward webhooks to your local environment:
```bash
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
```
This will provide you with a webhook signing secret that you can use in your local environment.
# plugins: Username
URL: /docs/plugins/username
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/username.mdx
Username plugin
***
title: Username
description: Username plugin
----------------------------
The username plugin is a lightweight plugin that adds username support to the email and password authenticator. This allows users to sign in and sign up with their username instead of their email.
## Installation
### Add Plugin to the server
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
username() // [!code highlight]
] // [!code highlight]
})
```
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
```bash
npx @better-auth/cli migrate
```
```bash
npx @better-auth/cli generate
```
See the [Schema](#schema) section to add the fields manually.
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { usernameClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
usernameClient() // [!code highlight]
] // [!code highlight]
})
```
## Usage
### Sign up
To sign up a user with username, you can use the existing `signUp.email` function provided by the client.
The `signUp` function should take a new `username` property in the object.
### Client Side
```ts
const { data, error } = await authClient.signUp.email({
email: email@domain.com,
name: Test User,
password: password1234,
username: test,
displayUsername: Test User123, // required
});
```
### Server Side
```ts
const data = await auth.api.signUpEmail({
body: {
email: email@domain.com,
name: Test User,
password: password1234,
username: test,
displayUsername: Test User123, // required
}
});
```
### Type Definition
```ts
type signUpEmail = {
/**
* The email of the user.
*/
email: string = "email@domain.com"
/**
* The name of the user.
*/
name: string = "Test User"
/**
* The password of the user.
*/
password: string = "password1234"
/**
* The username of the user.
*/
username: string = "test"
/**
* An optional display username of the user.
*/
displayUsername?: string = "Test User123"
}
```
If only `username` is provided, the `displayUsername` will be set to the pre normalized version of the `username`. You can see the [Username Normalization](#username-normalization) and [Display Username Normalization](#display-username-normalization) sections for more details.
### Sign in
To sign in a user with username, you can use the `signIn.username` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.signIn.username({
username: test,
password: password1234,
});
```
### Server Side
```ts
const data = await auth.api.signInUsername({
body: {
username: test,
password: password1234,
}
});
```
### Type Definition
```ts
type signInUsername = {
/**
* The username of the user.
*/
username: string = "test"
/**
* The password of the user.
*/
password: string = "password1234"
}
```
### Update username
To update the username of a user, you can use the `updateUser` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.updateUser({
username: new-username, // required
});
```
### Server Side
```ts
const data = await auth.api.updateUser({
body: {
username: new-username, // required
}
});
```
### Type Definition
```ts
type updateUser = {
/**
* The username to update.
*/
username?: string = "new-username"
}
```
### Check if username is available
To check if a username is available, you can use the `isUsernameAvailable` function provided by the client.
### Client Side
```ts
const { data, error } = await authClient.isUsernameAvailable({
username: new-username,
});
```
### Server Side
```ts
const response = await auth.api.isUsernameAvailable({
body: {
username: new-username,
}
});
```
### Type Definition
```ts
type isUsernameAvailable = {
/**
* The username to check.
*/
username: string = "new-username"
}
```
## Options
### Min Username Length
The minimum length of the username. Default is `3`.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
minUsernameLength: 5
})
]
})
```
### Max Username Length
The maximum length of the username. Default is `30`.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
maxUsernameLength: 100
})
]
})
```
### Username Validator
A function that validates the username. The function should return false if the username is invalid. By default, the username should only contain alphanumeric characters, underscores, and dots.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
usernameValidator: (username) => {
if (username === "admin") {
return false
}
return true
}
})
]
})
```
### Display Username Validator
A function that validates the display username. The function should return false if the display username is invalid. By default, no validation is applied to display username.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
displayUsernameValidator: (displayUsername) => {
// Allow only alphanumeric characters, underscores, and hyphens
return /^[a-zA-Z0-9_-]+$/.test(displayUsername)
}
})
]
})
```
### Username Normalization
A function that normalizes the username, or `false` if you want to disable normalization.
By default, usernames are normalized to lowercase, so "TestUser" and "testuser", for example, are considered the same username. The `username` field will contain the normalized (lower case) username, while `displayUsername` will contain the original `username`.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
usernameNormalization: (username) => {
return username.toLowerCase()
.replaceAll("0", "o")
.replaceAll("3", "e")
.replaceAll("4", "a");
}
})
]
})
```
### Display Username Normalization
A function that normalizes the display username, or `false` to disable normalization.
By default, display usernames are not normalized. When only `username` is provided during signup or update, the `displayUsername` will be set to match the original `username` value (before normalization). You can also explicitly set a `displayUsername` which will be preserved as-is. For custom normalization, provide a function that takes the display username as input and returns the normalized version.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
displayUsernameNormalization: (displayUsername) => displayUsername.toLowerCase(),
})
]
})
```
### Validation Order
By default, username and display username are validated before normalization. You can change this behavior by setting `validationOrder` to `post-normalization`.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
username({
validationOrder: {
username: "post-normalization",
displayUsername: "post-normalization",
}
})
]
})
```
## Schema
The plugin requires 2 fields to be added to the user table: