docs/content/docs/concepts/users-accounts.mdx
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.
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",
})
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,
})
}
}
})
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.
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}`
})
}
}
},
// ...
})
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
}
}
})
Use the changeEmail function on the client to initiate the process.
import { authClient } from "@/lib/auth-client"
await authClient.changeEmail({
newEmail: "[email protected]",
callbackURL: "/dashboard", // to redirect after verification
});
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:
<APIMethod path="/change-password" method="POST" requireSession> ```ts type changePassword = { /** * The new password to set */ newPassword: string = "newpassword1234" /** * The current user password */ currentPassword: string = "oldpassword1234" /** * When set to true, all other active sessions for this user will be invalidated */ revokeOtherSessions?: boolean = true } ``` </APIMethod>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
});
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
});
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: { // [!code highlight]
enabled: true // [!code highlight]
} // [!code highlight]
}
})
Once enabled, you can call authClient.deleteUser to permanently delete user data from your database.
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:
sendDeleteAccountVerification is 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
});
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
});
To delete a user, the user must meet one of the following requirements:
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"
});
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.
import { authClient } from "@/lib/auth-client"
await authClient.deleteUser();
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();
sendDeleteAccountVerification callback.
Then you need to call the deleteUser method with the token to complete the deletion.import { authClient } from "@/lib/auth-client"
await authClient.deleteUser({
token
});
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
},
},
},
});
Better Auth supports multiple authentication methods. Each authentication method is called a provider. For example, email and password authentication is a provider, Google authentication is a provider, etc.
When a user signs in using a provider, an account is created for the user. The account stores the authentication data returned by the provider. This data includes the access token, refresh token, and other information returned by the provider.
The account table stores the authentication data of the user Click here to view the schema
To list user accounts you can use client.user.listAccounts method. Which will return all accounts associated with a user.
import { authClient } from "@/lib/auth-client"
const accounts = await authClient.listAccounts();
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) // [!code highlight]
withEncryptedTokens.accessToken = encryptedAccessToken;
}
if (account.refreshToken) {
const encryptedRefreshToken = encrypt(account.refreshToken); // [!code highlight]
withEncryptedTokens.refreshToken = encryptedRefreshToken;
}
return {
data: withEncryptedTokens
}
},
}
}
}
})
Then whenever you retrieve back the account make sure to decrypt the tokens before using them.
Account linking 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,
}
},
});
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"]
}
},
});
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:
account_not_linked error instead of being silently linked, even when the provider is in trustedProviders or the email is verified.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,
}
},
});
Users already signed in can manually link their account to additional social providers or credential-based accounts.
Linking Social Accounts: Use the linkSocial method on the client to link a social provider to the user's account.
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
});
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:
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.
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 updateUserInfoOnLink to 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 your mapProfileToUser adds). The user's email and emailVerified are never changed on a link, so linking a provider can't rebind the account's identity.
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 setPassword method on the server.
import { auth } from "@/lib/auth"
await auth.api.setPassword({
body: {
newPassword: "new-password", // required
},
headers: await headers() // headers containing the user's session token
});
You can unlink a user account by providing a providerId.
import { authClient } from "@/lib/auth-client"
await authClient.unlinkAccount({
providerId: "google"
});
// Unlink a specific account
await authClient.unlinkAccount({
providerId: "google",
accountId: "123"
});
If the account doesn't exist, it will throw an error. Additionally, if the user only has one account, unlinking will be prevented to stop account lockout (unless allowUnlinkingAll is set to true).
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
accountLinking: {
allowUnlinkingAll: true
}
},
});