You are currently viewing documentation for v1.8 (Beta)
User & Accounts
Learn how to manage users and accounts, including updating user info, changing emails and passwords, deleting users with verification, token encryption, and account linking and unlinking.
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.
The user table can be extended using additional fields 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:
import { authClient } from "@/lib/auth-client"
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:
import { betterAuth } from "better-auth";
import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
}
},
emailVerification: {
// Required to send the verification email
sendVerificationEmail: async ({ user, url, token }) => {
void sendEmail({
to: user.email,
})
}
}
})Avoid awaiting the email sending to prevent
timing attacks. On serverless platforms, use waitUntil or similar to ensure the email is sent.
By default, when a user requests to change their email, a verification email is sent to the new email address. The email is only updated after the user verifies the new email.
Confirming with Current Email
For added security, you can require users to confirm the change via their current email before
the verification email is sent to the new address. To do this, provide the sendChangeEmailConfirmation function.
import { betterAuth } from "better-auth";
import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
sendChangeEmailConfirmation: async ({ user, newEmail, url, token }, request) => {
void sendEmail({
to: user.email, // Sent to the CURRENT email
subject: 'Approve email change',
text: `Click the link to approve the change to ${newEmail}: ${url}`
})
}
}
},
// ...
})Updating Without Verification
If you want to allow users to update their email immediately without verification (only if their current email is NOT verified), you can enable updateEmailWithoutVerification.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
updateEmailWithoutVerification: true
}
}
})If updateEmailWithoutVerification is false (default), the email will not be updated until the new email is verified, even if the current email is unverified.
Client Usage
Use the changeEmail function on the client to initiate the process.
import { authClient } from "@/lib/auth-client"
await authClient.changeEmail({
newEmail: "new-email@email.com",
callbackURL: "/dashboard", // to redirect after verification
});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:
const { data, error } = await authClient.changePassword({ newPassword: "newpassword1234", // required currentPassword: "oldpassword1234", // required revokeOtherSessions: true,});newPasswordstringrequiredThe new password to set
currentPasswordstringrequiredThe current user password
revokeOtherSessionsbooleanWhen set to true, all other active sessions for this user will be invalidated
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.
import { auth } from "@/lib/auth"
await auth.api.setPassword({
body: {
newPassword: "new-password",
},
headers: await headers() // headers containing the user's session token
});Verify Password
The verifyPassword function allows you to verify a user's current password. This is useful for confirming user identity before performing sensitive operations like updating security settings. This function can only be called from the server.
import { auth } from "@/lib/auth"
await auth.api.verifyPassword({
body: {
password: "user-password" // required
},
headers: await headers() // headers containing the user's session token
});For OAuth users who don't have passwords, consider using email verification or fresh session checks for sensitive operations instead.
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
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other config
user: {
deleteUser: {
enabled: true
}
}
})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:
import { betterAuth } from "better-auth";
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
sendDeleteAccountVerificationis a pre-generated link that deletes the user data when accessed.
import { authClient } from "@/lib/auth-client"
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.
import { authClient } from "@/lib/auth-client"
await authClient.deleteUser({
token
});Authentication Requirements
To delete a user, the user must meet one of the following requirements:
- A valid password
if the user has a password, they can delete their account by providing the password.
import { authClient } from "@/lib/auth-client"
await authClient.deleteUser({
password: "password"
});- 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.
import { authClient } from "@/lib/auth-client"
await authClient.deleteUser();- 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.
import { authClient } from "@/lib/auth-client"
await authClient.deleteUser();- If you have a custom delete account page and sent that url via the
sendDeleteAccountVerificationcallback. Then you need to call thedeleteUsermethod with the token to complete the deletion.
import { authClient } from "@/lib/auth-client"
await authClient.deleteUser({
token
});Callbacks
validateUserInfo: A gate that decides which identities Better Auth admits. It fires just before a user is created (create-user) or a new provider account is linked (link-account), for every authentication method (OAuth, OIDC SSO, SAML SSO, email/password, magic link, email OTP, anonymous, SIWE, phone number, admin-created users, and SCIM), including stateless setups with no persistent database, so policy lives in one place instead of per provider.
It also fires when an existing OAuth or SSO user signs in again (sign-in), and there it receives the fresh provider email and profile rather than the stored row. That lets a domain or org policy reject a user whose provider identity moved out of bounds (for example, an email that left the allowed domain). Non-provider returning sign-ins are not re-validated, because their stored row has not changed since create-user gated it; use the admin plugin's ban controls or a databaseHooks.session.create.before hook to block those.
source.action is "create-user", "link-account", or "sign-in", and source.method is the authentication method. For OAuth, source.oauth carries the provider id and raw provider profile. For OIDC and SAML SSO, source.sso carries the SSO provider id and raw provider claims or assertion attributes.
Return nothing to allow provisioning. Return an object with error to reject it: browser/redirect flows send the rejection to the configured error URL, while programmatic flows return a 403 API error. Avoid putting sensitive details in errorDescription because it is returned to the client.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
user: {
validateUserInfo: ({ user, source }) => {
if (!user.email?.endsWith("@example.com")) {
return {
error: "email_not_allowed",
errorDescription: "Use your example.com email to sign in",
};
}
if (
source.oauth?.providerId === "company-oauth" &&
source.oauth?.profile?.hd !== "example.com"
) {
return {
error: "invalid_organization",
errorDescription: "Use your company OAuth account",
};
}
},
},
});validateUserInfo is the high-level policy gate. The lower-level databaseHooks.user.create.before still runs afterward for data shaping and can also abort a write; reach for it when you need to mutate the record rather than accept or reject the identity.
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.
import { betterAuth } from "better-auth";
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.
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.
import { betterAuth } from "better-auth";
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 through providers such as email and password, Google, or an enterprise identity provider. Each method linked to a user is stored as an account.
An account has a local record ID and a provider identity. id identifies the Better Auth account record and is the value to pass as accountId to account-management APIs. The pair of issuer and accountId identifies the external account: issuer names the trusted authority, and accountId is the stable identifier that authority assigned. The providerId identifies the provider configuration Better Auth uses for protocol operations.
OAuth providers without a trusted issuer use local:oauth:<encoded providerId> as their account namespace, with the provider ID segment percent-encoded. Credential accounts use local:credential; do not use that credential namespace for an OAuth provider.
This separation allows multiple provider configurations for the same issuer to deduplicate the same external identity without treating an identifier from another issuer as the same person. Provider aliases share one account row and token set; they do not have independent grants or provider lifecycle records. See the account schema for the complete set of fields.
List User Accounts
Use listAccounts to retrieve every authentication method linked to the current user. Keep the returned id when you need to unlink the account or call another account-specific API.
import { authClient } from "@/lib/auth-client"
const { data: accounts, error } = await authClient.listAccounts();
if (error) {
throw new Error(error.message);
}
const googleAccount = accounts?.find((account) => account.providerId === "google");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.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
databaseHooks: {
account: {
create: {
before(account, context) {
const withEncryptedTokens = { ...account };
if (account.accessToken) {
const encryptedAccessToken = encrypt(account.accessToken)
withEncryptedTokens.accessToken = encryptedAccessToken;
}
if (account.refreshToken) {
const encryptedRefreshToken = encrypt(account.refreshToken);
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 is enabled by default and lets users 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.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
accountLinking: {
enabled: false,
}
},
});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.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"]
}
},
});Disable Implicit Linking
By default, when a user signs in with an OAuth provider whose email matches an existing user (and either the provider verified the email or it is in trustedProviders), Better Auth automatically links the OAuth account to that user. Set disableImplicitLinking: true to turn this off. With this option enabled:
- Same-email OAuth sign-ins for an existing user are rejected with the
account_not_linkederror instead of being silently linked, even when the provider is intrustedProvidersor the email is verified. - New users (no existing user with that email) can still sign up via OAuth.
- An already-authenticated user can still link providers explicitly via
linkSocial().
Use this when you want users to confirm linking from a settings page rather than implicitly on sign-in.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
accountLinking: {
disableImplicitLinking: true,
}
},
});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
linkSocialmethod on the client to link a social provider to the user's account.import { authClient } from "@/lib/auth-client" 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:
import { authClient } from "@/lib/auth-client" await authClient.linkSocial({ provider: "google", callbackURL: "/callback", scopes: ["https://www.googleapis.com/auth/drive.readonly"] // Request additional scopes });Newly granted scopes are merged into
account.scope, so prior grants survive incremental authorization. Sign-in re-authentication and refresh-token responses do not modifyaccount.scope.You can also link accounts using ID tokens directly, without redirecting to the provider's OAuth flow:
import { authClient } from "@/lib/auth-client" 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.
auth.ts import { betterAuth } from "better-auth"; export const auth = betterAuth({ account: { accountLinking: { allowDifferentEmails: true } }, });By default, linking an account leaves the existing user profile untouched. Enable
updateUserInfoOnLinkto copy the provider's profile onto the user each time an account is linked. The synced fields are the same ones persisted on sign-up (name,image, and any input-allowed fields yourmapProfileToUseradds). The user'semailandemailVerifiedare never changed on a link, so linking a provider can't rebind the account's identity.auth.ts import { betterAuth } from "better-auth"; 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
setPasswordmethod on the server.set-password.ts import { auth } from "@/lib/auth" await auth.api.setPassword({ body: { newPassword: "new-password", // required }, headers: await headers() // headers containing the user's session token });
setPassword can't be called from the client for security reasons.
Account Unlinking
Unlink an account by passing the Better Auth account record's id, which you can obtain from listAccounts.
import { authClient } from "@/lib/auth-client"
const { data: accounts, error } = await authClient.listAccounts();
if (error) {
throw new Error(error.message);
}
const account = accounts?.find((account) => account.providerId === "google");
if (account) {
await authClient.unlinkAccount({
accountId: account.id,
});
}If the account does not exist or does not belong to the current user, Better Auth returns an error. Better Auth also prevents a user from unlinking their only account unless allowUnlinkingAll is true.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
accountLinking: {
allowUnlinkingAll: true
}
},
});