# User & Accounts (/docs/concepts/users-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](/docs/concepts/database#user).

The user table can be extended using [additional fields](/docs/concepts/database#extending-core-schema) or by plugins to store additional data.

## Update User [#update-user]

### Update User Information [#update-user-information]

To update user information, you can use the `updateUser` function provided by the client. The `updateUser` function takes an object with the following properties:

```ts
import { authClient } from "@/lib/auth-client"

await authClient.updateUser({
    image: "https://example.com/image.jpg",
    name: "John Doe",
})
```

### Change Email [#change-email]

To allow users to change their email, first enable the `changeEmail` feature, which is disabled by default. Set `changeEmail.enabled` to `true`:

```ts title="auth.ts"
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,
            })
        }
    }
})
```

<Callout type="warn">
  Avoid awaiting the email sending to prevent
  timing attacks. On serverless platforms, use `waitUntil` or similar to ensure the email is sent.
</Callout>

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 [#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.

```ts title="auth.ts"
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 [#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`.

```ts title="auth.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    user: {
        changeEmail: {
            enabled: true,
            updateEmailWithoutVerification: true
        }
    }
})
```

<Callout type="warn">
  If `updateEmailWithoutVerification` is false (default), the email will not be updated until the new email is verified, even if the current email is unverified.
</Callout>

#### Client Usage [#client-usage]

Use the `changeEmail` function on the client to initiate the process.

```ts
import { authClient } from "@/lib/auth-client"

await authClient.changeEmail({
    newEmail: "new-email@email.com",
    callbackURL: "/dashboard", // to redirect after verification
});
```

### Change Password [#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:

**Endpoint:** `POST /change-password`

### Client Side

```ts
const { data, error } = await authClient.changePassword({
    newPassword: "newpassword1234", // required, The new password to set
    currentPassword: "oldpassword1234", // required, The current user password
    revokeOtherSessions: true, // When set to true, all other active sessions for this user will be invalidated
});
```

### Server Side

```ts
const data = await auth.api.changePassword({
    body: {
        newPassword: "newpassword1234", // required, The new password to set
        currentPassword: "oldpassword1234", // required, The current user password
        revokeOtherSessions: true, // When set to true, all other active sessions for this user will be invalidated
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type changePassword = {
    /**
     * The new password to set 
     */
    newPassword: string = "newpassword1234"
    /**
     * The current user password 
     */
    currentPassword: string = "oldpassword1234"
    /**
     * When set to true, all other active sessions for this user will be invalidated
     */
    revokeOtherSessions?: boolean = true
}
```

### Set Password [#set-password]

If a user was registered using OAuth or other providers, they won't have a password or a credential account. In this case, you can use the `setPassword` action to set a password for the user. For security reasons, this function can only be called from the server. We recommend having users go through a 'forgot password' flow to set a password for their account.

```ts title="set-password.ts"
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 [#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.

```ts title="verify-password.ts"
import { auth } from "@/lib/auth"

await auth.api.verifyPassword({
    body: {
        password: "user-password" // required
    },
    headers: await headers() // headers containing the user's session token
});
```

<Callout type="info">
  For OAuth users who don't have passwords, consider using email verification or fresh session checks for sensitive operations instead.
</Callout>

## Delete User [#delete-user]

Better Auth provides a utility to hard delete a user from your database. It's disabled by default, but you can enable it easily by passing `enabled:true`

```ts title="auth.ts"
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.

### Adding Verification Before Deletion [#adding-verification-before-deletion]

For added security, you’ll likely want to confirm the user’s intent before deleting their account. A common approach is to send a verification email. Better Auth provides a `sendDeleteAccountVerification` utility for this purpose.
This is especially needed if you have OAuth setup and want them to be able to delete their account without forcing them to login again for a fresh session.

Here’s how you can set it up:

```ts title="auth.ts"
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 `sendDeleteAccountVerification` is a pre-generated link that deletes the user data when accessed.

```ts
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.

```ts
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser({
    token
});
```

### Authentication Requirements [#authentication-requirements]

To delete a user, the user must meet one of the following requirements:

1. A valid password

if the user has a password, they can delete their account by providing the password.

```ts
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser({
    password: "password"
});
```

2. Fresh session

The user must have a `fresh` session token, meaning the user must have signed in recently. This is checked if the password is not provided.

<Callout type="warn">
  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.
</Callout>

```ts
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser();
```

3. Enabled email verification (needed for OAuth users)

As OAuth users don't have a password, we need to send a verification email to confirm the user's intent to delete their account. If you have already added the `sendDeleteAccountVerification` callback, you can just call the `deleteUser` method without providing any other information.

```ts
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser();
```

4. If you have a custom delete account page and sent that url via the `sendDeleteAccountVerification` callback.
   Then you need to call the `deleteUser` method with the token to complete the deletion.

```ts
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser({
    token
});
```

### Callbacks [#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.

```ts title="auth.ts"
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",
                };
            }
        },
    },
});
```

<Callout type="info">
  `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.
</Callout>

**beforeDelete**: This callback is called before the user is deleted. You can use this callback to perform any cleanup or additional checks before deleting the user.

```ts title="auth.ts"
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.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user, request) => {
                if (user.email.includes("admin")) {
                    throw new APIError("BAD_REQUEST", {
                        message: "Admin accounts can't be deleted",
                    });
                }
            },
        },
    },
});
```

**afterDelete**: This callback is called after the user is deleted. You can use this callback to perform any cleanup or additional actions after the user is deleted.

```ts title="auth.ts"
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 [#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.

| Field        | Responsibility                                                                   |
| ------------ | -------------------------------------------------------------------------------- |
| `providerId` | Selects the Better Auth provider or SSO connection used for protocol operations. |
| `accountId`  | Stores the stable subject assigned by that provider.                             |
| `issuer`     | Stores the namespace paired with `accountId` in the unique account identity key. |

providerId identifies the configured connection; accountId is the provider subject; issuer stores the identity namespace—verified authority under issuer strategy, deterministic provider namespace under provider-id strategy.

Both strategies use the same required fields and unique `(issuer, accountId)` index. Only the value persisted in `issuer` changes:

| Strategy        | `providerId`       | `accountId` | `issuer`                       |
| --------------- | ------------------ | ----------- | ------------------------------ |
| `"issuer"`      | `workforce-google` | `104925...` | `https://accounts.google.com`  |
| `"provider-id"` | `workforce-google` | `104925...` | `local:oauth:workforce-google` |

An account also has a local record ID. `id` identifies the Better Auth account record and is the value to pass as `accountId` to account-management APIs.

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.

Under the issuer strategy, 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. Those provider aliases share one account row and token set; they do not have independent grants or provider lifecycle records. Under provider-id, aliases retain separate synthetic namespaces. See the [account schema](/docs/concepts/database#account) for the complete set of fields.

The `auth init` command generates `account: { identityStrategy: "provider-id" }` for new projects. The v1.7 runtime remains compatible with configurations that omit `account.identityStrategy`: it uses `"issuer"` and emits a one-time warning asking the application to make the choice explicit. Applications migrating populated Better Auth 1.6 data should choose explicitly. The recommended compatibility path is `"provider-id"`, which persists `local:oauth:<encoded providerId>` for external accounts so provider aliases remain separate identities. Provider token, signature, issuer, audience, and subject verification does not change; the strategy selects the namespace stored after verification succeeds.

Keep the selected strategy consistent across every application instance and migration. Changing strategy on populated v1.7 data is an account re-key migration, not a configuration-only toggle. See the [1.7 upgrade guide](/docs/guides/1-7-upgrade-guide#choose-account-identity-strategy) before changing it.

### List User Accounts [#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.

```ts
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 [#token-encryption]

Better Auth doesn’t encrypt tokens by default and that’s intentional. We want you to have full control over how encryption and decryption are handled, rather than baking in behavior that could be confusing or limiting. If you need to store encrypted tokens (like accessToken or refreshToken), you can use databaseHooks to encrypt them before they’re saved to your database.

```ts
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 [#account-linking]

Account linking is [enabled by default](https://www.better-auth.com/docs/reference/options#accountlinking) 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.

```ts title="auth.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            enabled: false, 
        }
    },
});
```

#### Forced Linking [#forced-linking]

You can specify a list of "trusted providers." When a user logs in using a trusted provider, their account will be automatically linked even if the provider doesn’t confirm the email verification status. Use this with caution as it may increase the risk of account takeover.

```ts title="auth.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            enabled: true,
            trustedProviders: ["google", "github"]
        }
    },
});
```

#### Disable Implicit Linking [#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_linked`](/docs/reference/errors/account_not_linked) error instead of being silently linked, even when the provider is in `trustedProviders` or 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()`](#manually-linking-accounts).

Use this when you want users to confirm linking from a settings page rather than implicitly on sign-in.

```ts title="auth.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            disableImplicitLinking: true,
        }
    },
});
```

#### Manually Linking Accounts [#manually-linking-accounts]

Users already signed in can manually link their account to additional social providers or credential-based accounts.

* **Linking Social Accounts:** Use the `linkSocial` method on the client to link a social provider to the user's account.

  ```ts
  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:

  ```ts
  import { authClient } from "@/lib/auth-client"

  await authClient.linkSocial({
      provider: "google",
      callbackURL: "/callback",
      scopes: ["https://www.googleapis.com/auth/drive.readonly"] // Request additional scopes
  });
  ```

  <Callout type="info">
    Newly granted scopes are merged into `account.scope`, so prior grants survive incremental authorization. Sign-in re-authentication and refresh-token responses do not modify `account.scope`.
  </Callout>

  You can also link accounts using ID tokens directly, without redirecting to the provider's OAuth flow:

  ```ts
  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.

  ```ts title="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 `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.

  ```ts title="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 `setPassword` method on the server.

  ```ts title="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
  });
  ```

<Callout>
  `setPassword` can't be called from the client for security reasons.
</Callout>

### Account Unlinking [#account-unlinking]

Unlink an account by passing the Better Auth account record's `id`, which you can obtain from `listAccounts`.

```ts
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`.

```ts title="auth.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            allowUnlinkingAll: true
        }
    },
});
```

