# Better Auth > Enhance your app's security with two-factor authentication. --- # Source: https://www.better-auth.com/llms.txt/docs/plugins/2fa.md # Two-Factor Authentication (2FA) 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. npm pnpm yarn bun ```bash npx @better-auth/cli migrate ``` ```bash pnpm dlx @better-auth/cli migrate ``` ```bash yarn dlx @better-auth/cli migrate ``` ```bash bun x @better-auth/cli migrate ``` npm pnpm yarn bun ```bash npx @better-auth/cli generate ``` ```bash pnpm dlx @better-auth/cli generate ``` ```bash yarn dlx @better-auth/cli generate ``` ```bash bun x @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, // optional }); ``` ### Server Side ```ts const data = await auth.api.enableTwoFactor({ body: { password: secure-password, issuer: my-app-name, // optional }, // 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 verifying 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, // optional }); ``` ### Server Side ```ts const data = await auth.api.verifyTOTP({ body: { code: 012345, trustDevice, // optional } }); ``` ### 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 }, ctx) { // 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, // optional }); ``` ### Server Side ```ts const data = await auth.api.send2FaOTP({ body: { trustDevice, // optional } }); ``` ### 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, // optional }); ``` ### Server Side ```ts const data = await auth.api.verifyOTP({ body: { code: 012345, trustDevice, // optional } }); ``` ### 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 provide a backup code as an account recovery method. ### Client Side ```ts const { data, error } = await authClient.twoFactor.verifyBackupCode({ code: 123456, disableSession, // optional trustDevice, // optional }); ``` ### Server Side ```ts const data = await auth.api.verifyBackupCode({ body: { code: 123456, disableSession, // optional trustDevice, // optional } }); ``` ### 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 do 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, // optional }); ``` ### Server Side ```ts const data = await auth.api.viewBackupCodes({ body: { userId: user-id, // optional } }); ``` ### 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, 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 30 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 field 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. --- # Source: https://www.better-auth.com/llms.txt/docs/reference/errors/account_already_linked_to_different_user.md # account_already_linked_to_different_user The account is already linked to a different user. ## What is it? This error occurs during the OAuth flow when attempting to link an OAuth provider account to the currently authenticated user, but that exact provider account is already linked to another user in your project. To prevent account takeover, Better Auth blocks the link and throws this error. This situation is only possible through the OAuth flow (e.g., Google, GitHub, etc.). It is not triggered by email/password flows on their own. ## How to resolve ### Typical resolutions * Log in as the user who already has the provider linked, unlink the provider from that account, then link it to the intended account. * If both accounts belong to the same person and you want a single user, merge the accounts: choose a primary user, move sessions and linked accounts from the secondary user to the primary, then deactivate or delete the secondary. ### Common Causes * You previously signed in or signed up using this provider on a different user in the same project. * You have two local users (e.g., created via email/password or magic link) and you linked the provider to one of them; now you are trying to link the same provider to the other. * Test/preview environments share the same OAuth provider configuration and database; the provider account is already linked to a different user record. * Data migration or manual database edits left a stale link pointing to the wrong user. * You rely on email matching to decide linking, but the actual unique key is the provider account identifier (e.g., `providerId` + `accountId`). If that mapping exists for another user, linking will be blocked. ### Safer patterns and prevention * Avoid automatically linking a provider to whichever user is currently signed in unless you explicitly confirm ownership with the user. * If you provide a 'Connect account' UI, clearly communicate which user will receive the link and what to do if the provider is already linked elsewhere. * Consider disabling linking for providers you only want to use for sign-in, to avoid accidental cross-linking. ### Debug locally * Inspect your `account` database table. You should see rows keyed by `providerId` (e.g., 'google') and `accountId` (e.g., OIDC `sub`), pointing to a `userId`. * Identify which user currently owns the provider link and decide whether to unlink, merge, or keep as-is. * Verify your app is connected to the expected database and environment (dev/staging/prod) to avoid confusion due to shared credentials or misconfigured environment variables. ### Provider considerations * Ensure you request stable user identifiers from the provider (e.g., OIDC `openid` scope) so `accountId` remains consistent across sessions. * If you changed provider projects/tenants, identifiers may differ; confirm you are linking the correct provider credentials for the environment. This error is a security safeguard. It prevents an OAuth identity that already belongs to one user from being attached to another user without explicit action. If a legitimate merge is intended, perform a controlled merge or unlink-then-link flow rather than bypassing the check. --- # Source: https://www.better-auth.com/llms.txt/docs/plugins/admin.md # Admin 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. npm pnpm yarn bun ```bash npx @better-auth/cli migrate ``` ```bash pnpm dlx @better-auth/cli migrate ``` ```bash yarn dlx @better-auth/cli migrate ``` ```bash bun x @better-auth/cli migrate ``` npm pnpm yarn bun ```bash npx @better-auth/cli generate ``` ```bash pnpm dlx @better-auth/cli generate ``` ```bash yarn dlx @better-auth/cli generate ``` ```bash bun x @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, // optional data, // optional }); ``` ### Server Side ```ts const newUser = await auth.api.createUser({ body: { email: user@example.com, password: some-secure-password, name: James Smith, role: user, // optional data, // optional } }); ``` ### 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({ searchValue: some name, // optional searchField: name, // optional searchOperator: contains, // optional limit, // optional offset, // optional sortBy: name, // optional sortDirection: desc, // optional filterField: email, // optional filterValue: hello@example.com, // optional filterOperator: eq, // optional }); ``` ### Server Side ```ts const data = await auth.api.listUsers({ query: { searchValue: some name, // optional searchField: name, // optional searchOperator: contains, // optional limit, // optional offset, // optional sortBy: name, // optional sortDirection: desc, // optional filterField: email, // optional filterValue: hello@example.com, // optional filterOperator: eq, // optional }, // This endpoint requires session cookies. headers: await headers() }); ``` ### Type Definition ```ts type listUsers = { /** * 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, // optional role: admin, }); ``` ### Server Side ```ts const data = await auth.api.setRole({ body: { userId: user-id, // optional 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' } ``` ### Update user Update a user's details. ### Client Side ```ts const { data, error } = await authClient.admin.updateUser({ userId: user-id, data, }); ``` ### Server Side ```ts const data = await auth.api.adminUpdateUser({ body: { userId: user-id, data, }, // This endpoint requires session cookies. headers: await headers() }); ``` ### Type Definition ```ts type adminUpdateUser = { /** * The user id which you want to update. */ userId: string = "user-id" /** * The data to update. */ data: Record = { name: "John Doe" } ``` ### 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, // optional banExpiresIn, // optional }); ``` ### Server Side ```ts await auth.api.banUser({ body: { userId: user-id, banReason: Spamming, // optional banExpiresIn, // optional }, // 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, // optional role: admin, // optional permission, // optional }); ``` ### Server Side ```ts const data = await auth.api.userHasPermission({ body: { userId: user-id, // optional role: admin, // optional permission, // optional } }); ``` ### 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 when **not** using custom access control. Defaults to `["admin"]`. ```ts title="auth.ts" admin({ adminRoles: ["admin", "superadmin"], }); ``` **Note:** The `adminRoles` option is **not required** when using custom access control (via `ac` and `roles`). When you define custom roles with specific permissions, those roles will have exactly the permissions you grant them through the access control system. **Warning:** When **not** using custom access control, any role that isn't in the `adminRoles` list will **not** be able to perform admin operations. ### 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", }); ``` ### allowImpersonatingAdmins Whether to allow impersonating other admin users. Defaults to `false`. ```ts title="auth.ts" admin({ allowImpersonatingAdmins: true, }); ``` --- # Source: https://www.better-auth.com/llms.txt/docs/plugins/anonymous.md # Anonymous 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. npm pnpm yarn bun ```bash npx @better-auth/cli migrate ``` ```bash pnpm dlx @better-auth/cli migrate ``` ```bash yarn dlx @better-auth/cli migrate ``` ```bash bun x @better-auth/cli migrate ``` npm pnpm yarn bun ```bash npx @better-auth/cli generate ``` ```bash pnpm dlx @better-auth/cli generate ``` ```bash yarn dlx @better-auth/cli generate ``` ```bash bun x @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, }) ``` ### Delete Anonymous User To delete an anonymous user, you can call the `/delete-anonymous-user` endpoint. ### Client Side ```ts const { data, error } = await authClient.deleteAnonymousUser({}); ``` ### Server Side ```ts await auth.api.deleteAnonymousUser({}); ``` ### Type Definition ```ts type deleteAnonymousUser = { } ``` **Notes:** * The anonymous user is deleted by default when the account is linked to a new authentication method. * Setting `disableDeleteAnonymousUser` to `true` will prevent the anonymous user from being able to call the `/delete-anonymous-user` endpoint. ## Options ### `emailDomainName` The domain name to use when generating an email address for anonymous users. If not provided, the default format `temp@{id}.com` is used. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ plugins: [ anonymous({ emailDomainName: "example.com" // [!code highlight] -> temp-{id}@example.com }) ] }) ``` ### `generateRandomEmail` A custom function to generate email addresses for anonymous users. This allows you to define your own email format. The function can be synchronous or asynchronous. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ plugins: [ anonymous({ generateRandomEmail: () => { // [!code highlight] const id = crypto.randomUUID() // [!code highlight] return `guest-${id}@example.com` // [!code highlight] } // [!code highlight] }) ] }) ``` **Notes:** * If `generateRandomEmail` is provided, `emailDomainName` is ignored. * You are responsible for ensuring the email is unique to avoid conflicts. The returned email must be in a valid format. ### `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, when an anonymous user links their account to a new authentication method, the anonymous user record is automatically deleted. If you set this option to `true`, this automatic deletion will be disabled, and the `/delete-anonymous-user` endpoint will no longer be accessible to anonymous users. ### `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: --- # Source: https://www.better-auth.com/llms.txt/docs/plugins/api-key.md # API Key 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) * [Secondary storage support](/docs/plugins/api-key#secondary-storage) for high-performance API key lookups ## 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. npm pnpm yarn bun ```bash npx @better-auth/cli migrate ``` ```bash pnpm dlx @better-auth/cli migrate ``` ```bash yarn dlx @better-auth/cli migrate ``` ```bash bun x @better-auth/cli migrate ``` npm pnpm yarn bun ```bash npx @better-auth/cli generate ``` ```bash pnpm dlx @better-auth/cli generate ``` ```bash yarn dlx @better-auth/cli generate ``` ```bash bun x @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, // optional expiresIn, // optional userId: user-id, // optional prefix: project-api-key, // optional remaining, // optional metadata, // optional }); ``` ### Server Side ```ts const data = await auth.api.createApiKey({ body: { name: project-api-key, // optional expiresIn, // optional userId: user-id, // optional prefix: project-api-key, // optional remaining, // optional metadata, // optional } }); ``` ### 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, // optional }); ``` ### Server Side ```ts const data = await auth.api.verifyApiKey({ body: { key: your_api_key_here, permissions, // optional } }); ``` ### 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, // optional name: some-api-key-name, // optional enabled, // optional remaining, // optional refillAmount, // optional refillInterval, // optional metadata, // optional }); ``` ### Server Side ```ts const data = await auth.api.updateApiKey({ body: { keyId: some-api-key-id, userId: some-user-id, // optional name: some-api-key-name, // optional enabled, // optional remaining, // optional refillAmount, // optional refillInterval, // optional metadata, // optional } }); ``` ### 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, you can automatically create a mock session to represent the user by enabling `sessionForAPIKeys` option. This is generally not recommended, as it can lead to security issues if not used carefully. A leaked api key can be used to impersonate a user. **Rate Limiting Note**: When `enableSessionForAPIKeys` is enabled, the API key is validated once per request, and rate limiting is applied accordingly. If you manually verify an API key and then fetch a session separately, both operations will increment the rate limit counter. Using `enableSessionForAPIKeys` avoids this double increment. ```ts export const auth = betterAuth({ plugins: [ apiKey({ enableSessionForAPIKeys: true, }), ], }); ``` ```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"); }, }), ], }); ``` ## Storage Modes The API Key plugin supports multiple storage modes for flexible API key management, allowing you to choose the best strategy for your use case. ### Storage Mode Options #### `"database"` (Default) Store API keys only in the database adapter. This is the default mode and requires no additional configuration. ```ts export const auth = betterAuth({ plugins: [ apiKey({ storage: "database", // Default, can be omitted }), ], }); ``` #### `"secondary-storage"` Store API keys only in secondary storage (e.g., Redis). No fallback to database. Best for high-performance scenarios where all keys are migrated to secondary storage. ```ts import { createClient } from "redis"; import { betterAuth } from "better-auth"; import { apiKey } from "better-auth/plugins"; const redis = createClient(); await redis.connect(); export const auth = betterAuth({ secondaryStorage: { get: async (key) => await redis.get(key), set: async (key, value, ttl) => { if (ttl) await redis.set(key, value, { EX: ttl }); else await redis.set(key, value); }, delete: async (key) => await redis.del(key), }, plugins: [ apiKey({ storage: "secondary-storage", }), ], }); ``` #### Secondary Storage with Fallback Check secondary storage first, then fallback to database if not found. **Read behavior:** * Checks secondary storage first * If not found, queries the database * **Automatically populates secondary storage** when falling back to database (cache warming) * Ensures frequently accessed keys stay in cache over time **Write behavior:** * Writes to **both** database and secondary storage * Ensures consistency between both storage layers ```ts export const auth = betterAuth({ secondaryStorage: { get: async (key) => await redis.get(key), set: async (key, value, ttl) => { if (ttl) await redis.set(key, value, { EX: ttl }); else await redis.set(key, value); }, delete: async (key) => await redis.del(key), }, plugins: [ apiKey({ storage: "secondary-storage", fallbackToDatabase: true, }), ], }); ``` ### Custom Storage Methods You can provide custom storage methods specifically for API keys, overriding the global `secondaryStorage` configuration: ```ts export const auth = betterAuth({ plugins: [ apiKey({ storage: "secondary-storage", customStorage: { get: async (key) => { // Custom get logic for API keys return await customStorage.get(key); }, set: async (key, value, ttl) => { // Custom set logic for API keys await customStorage.set(key, value, ttl); }, delete: async (key) => { // Custom delete logic for API keys await customStorage.delete(key); }, }, }), ], }); ``` ## Rate Limiting Every API key can have its own rate limit settings. The built-in rate-limiting applies whenever an API key is validated, which includes: * When verifying an API key via the `/api-key/verify` endpoint * When using API keys for session creation (if `enableSessionForAPIKeys` is enabled), rate limiting applies to all endpoints that use the API key For other endpoints/methods that don't use API keys, you should utilize Better Auth's [built-in rate-limiting](/docs/concepts/rate-limit). **Double Rate-Limit Increment**: If you manually verify an API key using `verifyApiKey()` and then fetch a session using `getSession()` with the same API key header, both operations will increment the rate limit counter, resulting in two increments for a single request. To avoid this, either: * Use `enableSessionForAPIKeys: true` and let Better Auth handle session creation automatically (recommended) * Or verify the API key once and reuse the validated result instead of calling both methods separately 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? The rate limiting system uses a sliding window approach: 1. **First Request**: When an API key is used for the first time (no previous `lastRequest`), the request is allowed and `requestCount` is set to 1. 2. **Within Window**: For subsequent requests within the `timeWindow`, the `requestCount` is incremented. If `requestCount` reaches `rateLimitMax`, the request is rejected with a `RATE_LIMITED` error code. 3. **Window Reset**: If the time since the last request exceeds the `timeWindow`, the window resets: `requestCount` is reset to 1 and `lastRequest` is updated to the current time. 4. **Rate Limit Exceeded**: When a request is rejected due to rate limiting, the error response includes a `tryAgainIn` value (in milliseconds) indicating how long to wait before the window resets. **Disabling Rate Limiting**: * **Globally**: Set `rateLimit.enabled: false` in plugin options * **Per Key**: Set `rateLimitEnabled: false` when creating or updating an API key * **Null Values**: If `rateLimitTimeWindow` or `rateLimitMax` is `null`, rate limiting is effectively disabled for that key When rate limiting is disabled (globally or per-key), requests are still allowed but `lastRequest` is updated for tracking purposes. ## 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 when the interval has passed since the last refill (or since creation if no refill has occurred yet). 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` by default. This means that the API key will not be refilled automatically. However, if both `refillInterval` & `refillAmount` are set, then whenever the API key is used: * The system checks if the time since the last refill (or since creation if no refill has occurred) exceeds the `refillInterval` * If the interval has passed, the `remaining` count is reset to `refillAmount` (not incremented) * The `lastRefillAt` timestamp is updated to the current time #### 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` Whether 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` Whether 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. `enableSessionForAPIKeys` `boolean` An API Key can represent a valid session, so we can mock a session for the user if we find a valid API key in the request headers. Default is `false`. `storage` `"database" | "secondary-storage"` Storage backend for API keys. Default is `"database"`. * `"database"`: Store API keys in the database adapter (default) * `"secondary-storage"`: Store API keys in the configured secondary storage (e.g., Redis) `fallbackToDatabase` `boolean` When `storage` is `"secondary-storage"`, enable fallback to database if key is not found in secondary storage. Default is `false`. When `storage` is set to `"secondary-storage"`, you must configure `secondaryStorage` in your Better Auth options. API keys will be stored using key-value patterns: * `api-key:${hashedKey}` - Primary lookup by hashed key * `api-key:by-id:${id}` - Lookup by ID * `api-key:by-user:${userId}` - User's API key list If an API key has an expiration date (`expiresAt`), a TTL will be automatically set in secondary storage to ensure automatic cleanup. ```ts export const auth = betterAuth({ secondaryStorage: { get: async (key) => { return await redis.get(key); }, set: async (key, value, ttl) => { if (ttl) await redis.set(key, value, { EX: ttl }); else await redis.set(key, value); }, delete: async (key) => { await redis.del(key); }, }, plugins: [ apiKey({ storage: "secondary-storage", }), ], }); ``` `customStorage` `{ get: (key: string) => Promise | unknown; set: (key: string, value: string, ttl?: number) => Promise | void; delete: (key: string) => Promise | void; }` Custom storage methods for API keys. If provided, these methods will be used instead of `ctx.context.secondaryStorage`. Custom methods take precedence over global secondary storage. Useful when you want to use a different storage backend specifically for API keys, or when you need custom logic for storage operations. ```ts export const auth = betterAuth({ plugins: [ apiKey({ storage: "secondary-storage", customStorage: { get: async (key) => await customStorage.get(key), set: async (key, value, ttl) => await customStorage.set(key, value, ttl), delete: async (key) => await customStorage.delete(key), }, }), ], }); ``` `deferUpdates` `boolean` Defer non-critical updates (rate limiting counters, timestamps, remaining count) to run after the response is sent using the global `backgroundTasks` handler. This can significantly improve response times on serverless platforms. Default is `false`. Requires `backgroundTasks.handler` to be configured in the main auth options. Enabling this introduces eventual consistency where the response returns optimistic data before the database is updated. Only enable if your application can tolerate this trade-off for improved latency. ```ts import { waitUntil } from "@vercel/functions"; export const auth = betterAuth({ advanced: { backgroundTasks: { handler: waitUntil, }, } plugins: [ apiKey({ deferUpdates: true, }), ], }); ``` ```ts import { AsyncLocalStorage } from "node:async_hooks"; const execCtxStorage = new AsyncLocalStorage(); export const auth = betterAuth({ advanced: { backgroundTasks: { handler: waitUntil, }, } plugins: [ apiKey({ deferUpdates: true, }), ], }); // In your request handler, wrap with execCtxStorage.run(ctx, ...) ``` `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` --- # Source: https://www.better-auth.com/llms.txt/docs/concepts/api.md # API 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) } } ``` --- # Source: https://www.better-auth.com/llms.txt/docs/authentication/apple.md # Apple 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'. **Note:** Apple allows a maximum expiration of 6 months (180 days) for the client secret JWT. You will need to regenerate the client secret before it expires to maintain uninterrupted authentication. ### 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. **Localhost and Non-TLS Restrictions** Apple Sign In does **not** support `localhost` or non-HTTPS URLs. During development: * You cannot use `http://localhost` as a return URL * You must use a domain with valid HTTPS/TLS certificate This limitation is enforced by Apple's security requirements and cannot be bypassed. ## 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) --- # Source: https://www.better-auth.com/llms.txt/docs/integrations/astro.md # Source: https://www.better-auth.com/llms.txt/docs/examples/astro.md # Astro Example 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