Better Auth provides a set of methods for managing users beyond authentication. This includes updating user information, changing passwords, deleting users, and managing OAuth account connections.
The user table stores authentication data. See the database schema for the full field list. You can extend the user schema with additional fields or use plugins.
Updating users
Update a user’s name or image using the updateUser function:
import { authClient } from "@/lib/auth-client";
await authClient.updateUser({
image: "https://example.com/image.jpg",
name: "John Doe",
});
Change email
Email change is disabled by default. Enable it by setting changeEmail.enabled to true and providing a sendVerificationEmail function:
import { betterAuth } from "better-auth";
import { sendEmail } from "./email";
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
},
},
emailVerification: {
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 a similar mechanism to ensure the email is sent.
By default, a verification email is sent to the new email address. The email is only updated after the user verifies it.
Require confirmation from the current email
For added security, require users to approve the change from their current email before the verification link is sent to the new address:
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
sendChangeEmailConfirmation: async ({ user, newEmail, url, token }, request) => {
void sendEmail({
to: user.email, // current email
subject: "Approve email change",
text: `Click the link to approve the change to ${newEmail}: ${url}`,
});
},
},
},
});
Allow change without verification
If the user’s current email is not verified and you want to allow immediate updates without verification:
export const auth = betterAuth({
user: {
changeEmail: {
enabled: true,
updateEmailWithoutVerification: true,
},
},
});
Client usage
import { authClient } from "@/lib/auth-client";
await authClient.changeEmail({
newEmail: "new-email@example.com",
callbackURL: "/dashboard",
});
Change password
Passwords are stored in the account table, not the user table. Use changePassword to update it:
await authClient.changePassword({
newPassword: "newpassword1234",
currentPassword: "oldpassword1234",
revokeOtherSessions: true, // optional: sign out other devices
});
Set password
Users who registered via OAuth do not have a password. Use the server-side setPassword method to add one. For security, this can only be called from the server — we recommend routing users through a forgot-password flow instead.
import { auth } from "@/lib/auth";
await auth.api.setPassword({
body: {
newPassword: "new-password",
},
headers: await headers(),
});
Verify password
Verify the current user’s password before sensitive operations. This can only be called from the server.
import { auth } from "@/lib/auth";
await auth.api.verifyPassword({
body: {
password: "user-password",
},
headers: await headers(),
});
For OAuth users without passwords, consider using email verification or fresh session checks for sensitive operations.
Deleting users
User deletion is disabled by default. Enable it with deleteUser.enabled:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
},
},
});
Once enabled, call authClient.deleteUser() to permanently remove a user’s data.
Verification before deletion
For added security, send a verification email before deleting the account. This is especially useful for OAuth users who cannot prove identity with a password.
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
sendDeleteAccountVerification: async (
{ user, url, token },
request
) => {
// send email with url or custom link using token
},
},
},
});
// Client: trigger deletion via email verification
await authClient.deleteUser({
callbackURL: "/goodbye",
});
// Client: delete with a custom token
await authClient.deleteUser({ token });
Authentication requirements
To delete an account, the user must satisfy one of the following:
Provide a valid password
await authClient.deleteUser({ password: "password" });
Have a fresh session
The user must have signed in recently. The freshAge defaults to 1 day.await authClient.deleteUser();
Complete email verification
Required for OAuth users who have no password. Only works when sendDeleteAccountVerification is configured.await authClient.deleteUser();
Callbacks
Run custom logic before and after user deletion:
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: async (user, request) => {
// cleanup external resources
},
},
},
});
Accounts
Each authentication method is a provider. When a user signs in via a provider, an account record is created in the account table. Accounts store provider-returned data such as access tokens, refresh tokens, and scopes.
List user accounts
import { authClient } from "@/lib/auth-client";
const accounts = await authClient.listAccounts();
Token encryption
Better Auth does not encrypt tokens by default — this is intentional, giving you full control over encryption. Use databaseHooks to encrypt tokens before they are saved:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
databaseHooks: {
account: {
create: {
before(account, context) {
const withEncryptedTokens = { ...account };
if (account.accessToken) {
withEncryptedTokens.accessToken = encrypt(account.accessToken);
}
if (account.refreshToken) {
withEncryptedTokens.refreshToken = encrypt(account.refreshToken);
}
return { data: withEncryptedTokens };
},
},
},
},
});
Remember to decrypt tokens whenever you retrieve them from the database.
Account linking
Account linking is enabled by default and lets users connect multiple authentication methods to a single user record. Linking requires the provider to confirm the email as verified.
To disable account linking entirely:
export const auth = betterAuth({
account: {
accountLinking: {
enabled: false,
},
},
});
Trusted providers (forced linking)
Specify providers that are trusted regardless of email verification status:
export const auth = betterAuth({
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"],
},
},
});
Use trusted providers with caution. Forcing account linking without email verification increases the risk of account takeover.
Manually linking social accounts
Signed-in users can link additional social providers:
import { authClient } from "@/lib/auth-client";
await authClient.linkSocial({
provider: "google",
callbackURL: "/callback",
});
// Request additional scopes when linking
await authClient.linkSocial({
provider: "google",
callbackURL: "/callback",
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
});
// Link using an ID token (no redirect required)
await authClient.linkSocial({
provider: "google",
idToken: {
token: "id_token_from_provider",
accessToken: "access_token",
},
});
To allow linking with a different email address than the user’s current one:
export const auth = betterAuth({
account: {
accountLinking: {
allowDifferentEmails: true,
},
},
});
To update user information when a new account is linked:
export const auth = betterAuth({
account: {
accountLinking: {
updateUserInfoOnLink: true,
},
},
});
Linking credential-based accounts
For email/password accounts, use the server-side setPassword method or direct users through a forgot-password flow:
import { auth } from "@/lib/auth";
await auth.api.setPassword({
body: { newPassword: "new-password" },
headers: await headers(),
});
Account unlinking
Unlink a provider from the current user’s account:
import { authClient } from "@/lib/auth-client";
await authClient.unlinkAccount({
providerId: "google",
});
// Unlink a specific account by ID
await authClient.unlinkAccount({
providerId: "google",
accountId: "123",
});
If the user has only one account, unlinking is blocked by default to prevent lockout. Override this with allowUnlinkingAll:
export const auth = betterAuth({
account: {
accountLinking: {
allowUnlinkingAll: true,
},
},
});