# Database (/docs/concepts/database)

Learn about database adapters, migrations, secondary storage with Redis, core schema (user, session, account, verification), custom tables, extending schemas, ID generation, database hooks, and plugin schemas.



## Adapters [#adapters]

Better Auth connects to a database to store data. The database will be used to store data such as users, sessions, and more. Plugins can also define their own database tables to store data.

You can pass a database connection to Better Auth by passing a supported database instance in the database options. You can learn more about supported database adapters in the [Other relational databases](/docs/adapters/other-relational-databases) documentation.

<Callout type="info">
  Better Auth also works without any database. For more details, see [Stateless Session Management](/docs/concepts/session-management#stateless-session-management).
</Callout>

## CLI [#cli]

Better Auth comes with a CLI tool to manage database migrations and generate schema.

### Running Migrations [#running-migrations]

The cli checks your database and prompts you to add missing tables or update existing ones with new columns. This is only supported for the built-in Kysely adapter. For other adapters, you can use the `generate` command to create the schema and handle the migration through your ORM.

<CodeBlockTabs defaultValue="npm" groupId="persist-install">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx auth@latest migrate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx auth@latest migrate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx auth@latest migrate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x auth@latest migrate
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Callout type="info">
  For PostgreSQL users: The migrate command supports non-default schemas. It automatically detects your `search_path` configuration and creates tables in the correct schema. See [PostgreSQL adapter](/docs/adapters/postgresql#use-a-non-default-schema) for details.
</Callout>

### Generating Schema [#generating-schema]

Better Auth also provides a `generate` command to generate the schema required by Better Auth. If you're using a database adapter like Prisma or Drizzle, this command will generate the right schema for your ORM. If you're using the built-in Kysely adapter, it will generate an SQL file you can run directly on your database.

<CodeBlockTabs defaultValue="npm" groupId="persist-install">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx auth@latest generate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx auth@latest generate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx auth@latest generate
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x auth@latest generate
    ```
  </CodeBlockTab>
</CodeBlockTabs>

See the [CLI](/docs/concepts/cli) documentation for more information on the CLI.

<Callout>
  If you prefer adding tables manually, you can do that as well. The core schema
  required by Better Auth is described below and you can find additional schema
  required by plugins in the plugin documentation.
</Callout>

### Programmatic Migrations [#programmatic-migrations]

In environments where the CLI isn't available (e.g. Cloudflare Workers, serverless functions), you can run migrations programmatically using `getMigrations` from `better-auth/db/migration`.

```typescript
import { getMigrations } from "better-auth/db/migration";
import { auth } from "./auth";

const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(auth.options);

await runMigrations();
```

<Callout type="warn">
  `getMigrations` only works with the built-in Kysely adapter (SQLite/D1, PostgreSQL, MySQL, MSSQL). It does **not** work with Prisma or Drizzle ORM adapters — use CLI migrations with those ORMs instead.
</Callout>

<Accordions>
  <Accordion id="cloudflare-d1-example" title="Example: Cloudflare D1">
    [Cloudflare D1](https://developers.cloudflare.com/d1/) can only be queried through a Cloudflare Worker, so the CLI cannot access it directly. Instead, you can run programmatic migrations through an endpoint:

    ```typescript title="auth.ts"
    import { env } from "cloudflare:workers";
    import { betterAuth } from "better-auth";

    export const auth = betterAuth({
      database: env.DB,
      // ... rest of config
    });
    ```

    ```typescript title="src/index.ts"
    import { Hono } from "hono";
    import { auth } from "./auth";
    import { getMigrations } from "better-auth/db/migration";

    const app = new Hono();

    // Protect or remove this endpoint in production
    app.post("/migrate", async (c) => {
      try {
        const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(auth.options);

        if (toBeCreated.length === 0 && toBeAdded.length === 0) {
          return c.json({ message: "No migrations needed" });
        }

        await runMigrations();
        return c.json({
          message: "Migrations completed successfully",
          created: toBeCreated.map((t) => t.table),
          added: toBeAdded.map((t) => t.table),
        });
      } catch (error) {
        return c.json(
          { error: error instanceof Error ? error.message : "Migration failed" },
          500,
        );
      }
    });

    app.on(["POST", "GET"], "/api/auth/*", (c) => {
      return auth.handler(c.req.raw);
    });

    export default app;
    ```

    <Callout type="info">
      If you're using Cloudflare D1 with Drizzle or Prisma, use [`cloudflare:workers`](https://developers.cloudflare.com/workers/runtime-apis/bindings/) to access `env` and follow the guides below:

      * [Drizzle with Cloudflare D1 guide](https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit)
      * [Prisma with Cloudflare D1 guide](https://www.prisma.io/docs/guides/cloudflare-d1)
    </Callout>
  </Accordion>
</Accordions>

## Schema Validation [#schema-validation]

During initialization, Better Auth compares the schema with the tables it writes and reports missing tables, missing columns, and required columns it never fills, together with their fixes. Errors appear through your configured logger without waiting for an authentication request. Requests await the same check and fail if the schema does not match; validation does not automatically stop your server or build.

Validation is enabled by default, including in production, and caches a clean result or mismatch per adapter instance. Programmatic migrations invalidate cached checks for the same database instance. Requests waiting on an invalidated check await the new result. Restart after applying schema changes with other tools. Kysely reads live database metadata and needs database access during initialization. Drizzle checks the configured schema object and Prisma checks the generated client's data model, without querying the database. These local checks cannot detect migrations that were not applied to the database. The compact `prisma-client` model omits nullability, so required unwritten columns are reported by `auth generate` instead. Custom adapters without a registered check are not validated.

For adapters without schema validation, initialization logs a warning if you explicitly set `validateSchema: true`, or a debug message if you omit the option. Database operations proceed normally.

Set `advanced.database.validateSchema` to `false` to disable runtime validation and its skip message. `auth migrate` and `auth generate` retain their own schema diagnostics.

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

export const auth = betterAuth({
	advanced: {
		database: {
			validateSchema: false, // [!code highlight]
		},
	},
});
```

## Secondary Storage [#secondary-storage]

Secondary storage in Better Auth allows you to use key-value stores for managing session data, verification records, rate limiting counters, and other short-lived auth data. This can be useful when you want to offload the storage of intensive records to a high performance storage or even RAM.

### Implementation [#implementation]

To use secondary storage, implement the `SecondaryStorage` interface:

```typescript
interface SecondaryStorage {
  get: (key: string) => Promise<unknown>;
  getAndDelete: (key: string) => Promise<unknown>;
  increment: (key: string, ttl: number) => Promise<number>;
  set: (key: string, value: string, ttl?: number) => Promise<void>;
  delete: (key: string) => Promise<void>;
}
```

Then, provide your implementation to the `betterAuth` function:

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

betterAuth({
  // ... other options
  secondaryStorage: {
    // Your implementation here
  },
});
```

### Redis Storage [#redis-storage]

For most applications, we recommend using the official Redis storage package,
which uses [ioredis](https://github.com/redis/ioredis).

#### Official Redis Storage [#official-redis-storage]

<CodeBlockTabs defaultValue="npm" groupId="persist-install">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @better-auth/redis-storage ioredis
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @better-auth/redis-storage ioredis
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @better-auth/redis-storage ioredis
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @better-auth/redis-storage ioredis
    ```
  </CodeBlockTab>
</CodeBlockTabs>

```typescript title="auth.ts"
import { betterAuth } from "better-auth";
import { Redis } from "ioredis";
import { redisStorage } from "@better-auth/redis-storage";

const redis = new Redis(process.env.REDIS_URL!);

export const auth = betterAuth({
	// ... other options
	secondaryStorage: redisStorage({
		client: redis,
		keyPrefix: "better-auth:", // optional, defaults to "better-auth:"
	}),
});
```

#### Custom Implementations [#custom-implementations]

<Tabs items="[&#x22;node-redis&#x22;, &#x22;Upstash Redis&#x22;]">
  <Tab value="node-redis">
    If you're using Redis 7 or later, you can implement secondary storage with
    [node-redis](https://github.com/redis/node-redis):

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install redis
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add redis
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add redis
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add redis
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ```typescript title="auth.ts"
    import { betterAuth, type SecondaryStorage } from "better-auth";
    import { createClient } from "redis";

    const redis = createClient();
    await redis.connect();

    const redisSecondaryStorage: SecondaryStorage = {
      get(key) {
        return redis.get(key);
      },

      getAndDelete(key) {
        return redis.getDel(key);
      },

      async increment(key, ttl) {
        if (!Number.isInteger(ttl) || ttl <= 0) {
          throw new TypeError("Redis increment TTL must be a positive integer");
        }

        const [value] = await redis
          .multi()
          .incr(key)
          .expire(key, ttl, "NX")
          .execTyped();

        return value;
      },

      async set(key, value, ttl) {
        if (ttl) await redis.set(key, value, { EX: ttl });
        else await redis.set(key, value);
      },

      async delete(key) {
        await redis.del(key);
      },
    };

    export const auth = betterAuth({
      // ... other options
      secondaryStorage: redisSecondaryStorage,
    });
    ```
  </Tab>

  <Tab value="Upstash Redis">
    Install the Upstash Redis client:

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install @upstash/redis
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @upstash/redis
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @upstash/redis
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @upstash/redis
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` in your environment.
    See the [Upstash Redis TypeScript SDK guide](https://upstash.com/docs/redis/sdks/ts/getstarted)
    for additional configuration options.

    ```typescript title="auth.ts"
    import { Redis } from "@upstash/redis";
    import { betterAuth, type SecondaryStorage } from "better-auth";

    const redis = Redis.fromEnv();

    const redisSecondaryStorage: SecondaryStorage = {
      get(key) {
        return redis.get(key);
      },

      getAndDelete(key) {
        return redis.getdel(key);
      },

      async increment(key, ttl) {
        if (!Number.isInteger(ttl) || ttl <= 0) {
          throw new TypeError("Redis increment TTL must be a positive integer");
        }

        const [value] = await redis
          .multi()
          .incr(key)
          .expire(key, ttl, "NX")
          .exec();

        return value;
      },

      async set(key, value, ttl) {
        if (ttl) {
          await redis.set(key, value, { ex: ttl });
        } else {
          await redis.set(key, value);
        }
      },

      async delete(key) {
        await redis.del(key);
      },
    };

    export const auth = betterAuth({
      // ... other options
      secondaryStorage: redisSecondaryStorage,
    });
    ```
  </Tab>
</Tabs>

When implementing secondary storage directly, prefix its keys to avoid
collisions with other applications sharing the same Redis database.

## Core Schema [#core-schema]

Better Auth requires the following tables to be present in the database. The types are in `typescript` format. You can use corresponding types in your database.

### User [#user]

Table Name: `user`



<DatabaseTable name="user" fields="userTableFields" />

### Session [#session]

Table Name: `session`



<DatabaseTable name="session" fields="sessionTableFields" />

### Account [#account]

Table Name: `account`

An account represents one authentication method linked to a user. Better Auth recognizes the provider-side identity by the pair of `providerId` and `accountId`, while `id` identifies the local account row. Use `id` when an account API asks for an `accountId`.



<DatabaseTable name="account" fields="accountTableFields" />

Credential accounts use the `credential` provider ID and the linked user's stable `id` as `accountId`.

### Verification [#verification]

Table Name: `verification`



<DatabaseTable name="verification" fields="verificationTableFields" />

## Custom Tables [#custom-tables]

Better Auth allows you to customize the table names and column names for the core schema. You can also extend the core schema by adding additional fields to the user and session tables.

### Custom Table Names [#custom-table-names]

You can customize the table names and column names for the core schema by using the `modelName` and `fields` properties in your auth config:

```ts title="auth.ts"
export const auth = betterAuth({
  user: {
    modelName: "users",
    fields: {
      name: "full_name",
      email: "email_address",
    },
  },
  session: {
    modelName: "user_sessions",
    fields: {
      userId: "user_id",
    },
  },
});
```

<Callout>
  Type inference in your code will still use the original field names (e.g.,
  `user.name`, not `user.full_name`).
</Callout>

To customize table names and column name for plugins, you can use the `schema` property in the plugin config:

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

export const auth = betterAuth({
  plugins: [
    twoFactor({
      schema: {
        user: {
          fields: {
            twoFactorEnabled: "two_factor_enabled",
            secret: "two_factor_secret",
          },
        },
      },
    }),
  ],
});
```

### Extending Core Schema [#extending-core-schema]

Better Auth provides a type-safe way to extend the `user` and `session` schemas. You can add custom fields to your auth config, and the CLI will automatically update the database schema. These additional fields will be properly inferred in functions like `useSession`, `signUp.email`, and other endpoints that work with user or session objects.

To add custom fields, use the `additionalFields` property in the `user` or `session` object of your auth config. The `additionalFields` object uses field names as keys, with each value being a `FieldAttributes` object containing:

* `type`: The data type of the field (e.g., "string", "number", "boolean").
* `required`: A boolean indicating if the field is mandatory.
* `defaultValue`: The default value for the field (note: this only applies in the JavaScript layer; in the database, the field will be optional).
* `input`: Whether Better Auth accepts the field when creating or updating a record (default: `true`). For user fields, this includes values from API input and `mapProfileToUser`. Set this to `false` for server-owned fields such as `role`.
* `returned`: Whether Better Auth includes the stored field in response bodies (default: `true`). This does not affect whether input can write the field.

Here's an example of how to extend the user schema with additional fields:

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

export const auth = betterAuth({
  user: {
    additionalFields: {
      role: {
        type: ["user", "admin"],
        required: false,
        defaultValue: "user",
        input: false, // don't allow user to set role
      },
      lang: {
        type: "string",
        required: false,
        defaultValue: "en",
      },
    },
  },
});
```

Now you can access the additional fields in your application logic.

```ts
//on signup
const res = await auth.api.signUpEmail({
	body: {
		email: 'test@example.com',
		password: 'password',
		name: 'John Doe',
		lang: 'fr',
	},
});

//user object
res.user.role; // > "user"
res.user.lang; // > "fr"
```

<Callout>
  See the
  [TypeScript](/docs/concepts/typescript#inferring-additional-fields-on-client)
  documentation for more information on how to infer additional fields on the
  client side.
</Callout>

For `user.additionalFields`, `input` and `returned` are independent:

| Configuration                       | Input behavior                                                                                                                                                    | Response behavior             |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `{ input: true, returned: true }`   | API input and provider profile mapping can supply the field.                                                                                                      | The stored field is included. |
| `{ input: true, returned: false }`  | API input and provider profile mapping can supply the field.                                                                                                      | The stored field is omitted.  |
| `{ input: false, returned: true }`  | API input and provider profile mapping cannot supply the field. A configured `defaultValue` can initialize it; otherwise use an application-owned database write. | The stored field is included. |
| `{ input: false, returned: false }` | API input and provider profile mapping cannot supply the field. A configured `defaultValue` can initialize it; otherwise use an application-owned database write. | The stored field is omitted.  |

If you're using a social or OAuth provider, `mapProfileToUser` can populate additional fields from the provider profile when those fields allow input. The mapper runs on your server, but Better Auth still treats its return value as provider input. Fields marked `input: false` stay server-owned, provider values for those fields are ignored, and configured `defaultValue` values still apply when OAuth creates a user. See [server-owned fields and authorization claims](/docs/concepts/oauth#server-owned-fields-and-authorization-claims) for secure handling patterns.

**Example: Mapping Profile to User For `firstName` and `lastName`**

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

export const auth = betterAuth({
  socialProviders: {
    github: {
      clientId: "YOUR_GITHUB_CLIENT_ID",
      clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.name.split(" ")[0],
          lastName: profile.name.split(" ")[1],
        };
      },
    },
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.given_name,
          lastName: profile.family_name,
        };
      },
    },
  },
});
```

### ID Generation [#id-generation]

Better Auth by default will generate unique IDs for users, sessions, and other entities.
You can customize ID generation behavior using the `advanced.database.generateId` option.

#### Option 1: Let Database Generate IDs [#option-1-let-database-generate-ids]

Setting `generateId` to `false` allows your database handle all ID generation: (outside of `generateId` being `serial` and some cases of `generateId` being `uuid`)

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

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: false, // "serial" for auto-incrementing numeric IDs
    },
  },
});
```

#### Option 2: Custom ID Generation Function [#option-2-custom-id-generation-function]

Use a function to generate IDs. You can return `false` or `undefined` from the function to let the database generate the ID for specific models:

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

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: (options) => {
        // Let database auto-generate for specific models
        if (options.model === "user" || options.model === "users") {
          return false; // Let database generate ID
        }
        // Generate UUIDs for other tables
        return crypto.randomUUID();
      },
    },
  },
});
```

<Callout type="info">
  **Important**: Returning `false` or `undefined` from the `generateId` function lets the database handle ID generation for that specific model. Setting `generateId: false` (without a function) disables ID generation for **all** tables.
</Callout>

#### Option 3: Consistent Custom ID Generator [#option-3-consistent-custom-id-generator]

Generate the same type of ID for all tables:

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

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: () => crypto.randomUUID(),
    },
  },
});
```

### Numeric IDs [#numeric-ids]

If you prefer auto-incrementing numeric IDs, you can set the `advanced.database.generateId` option to `"serial"`.
Doing this will disable Better-Auth from generating IDs for any table, and will assume your
database will generate the numeric ID automatically.

When enabled, the Better-Auth CLI will generate or migrate the schema with the `id` field as a numeric type for your database
with auto-incrementing attributes associated with it.

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

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: "serial",
    },
  },
});
```

<Callout type="info">
  Better-Auth will continue to infer the type of the `id` field as a `string` for the database, but will
  automatically convert it to a numeric type when fetching or inserting data from the database.

  It's likely when grabbing `id` values returned from Better-Auth that you'll receive a string version of a number,
  this is normal. It's also expected that all id values passed to Better-Auth (eg via an endpoint body) is expected to be a string.
</Callout>

### UUIDs [#uuids]

If you prefer UUIDs for the `id` field, you can set the `advanced.database.generateId` option to `"uuid"`.
By default, Better-Auth will generate UUIDs for the `id` field for all tables, except adapters that use `PostgreSQL` where we allow the
database to generate the UUID automatically.

By enabling this option, the Better-Auth CLI will generate or migrate the schema with the `id` field as a UUID type for your database.
If the `uuid` type is not supported, we will generate a normal `string` type for the `id` field.

### Mixed ID Types [#mixed-id-types]

If you need different ID types across tables (e.g., integer IDs for users, UUID strings for sessions/accounts/verification), use a `generateId` callback function.

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

export const auth = betterAuth({
  database: db,
  user: {
    modelName: "users", // PostgreSQL: id serial primary key
  },
  session: {
    modelName: "session", // PostgreSQL: id text primary key
  },
  advanced: {
    database: {
      // Do NOT set useNumberId - it's global and affects all tables
      generateId: (options) => {
        if (options.model === "user" || options.model === "users") {
          return false; // Let PostgreSQL serial generate it
        }
        return crypto.randomUUID(); // UUIDs for session, account, verification
      },
    },
  },
});
```

This configuration allows you to:

* Use database auto-increment (serial, auto\_increment, etc.) for the users table
* Generate UUIDs for all other tables (session, account, verification)
* Maintain compatibility with existing schemas that use different ID types

<Callout type="info">
  **Use Case**: This is particularly useful when migrating from other authentication providers (like Clerk) where you have existing users with integer IDs but want UUID strings for new tables.
</Callout>

### Database Hooks [#database-hooks]

Database hooks allow you to define custom logic that can be executed during the lifecycle of core database operations in Better Auth. You can create hooks for the following models: **user**, **session**, and **account**.

<Callout type="warn">
  Additional fields are supported, however full type inference for these fields isn't yet supported.
  Improved type support is planned.
</Callout>

There are two types of hooks you can define:

#### 1. Before Hook [#1-before-hook]

* **Purpose**: This hook is called before the respective entity (user, session, or account) is created, updated, or deleted.
* **Behavior**: If the hook returns `false`, the operation will be aborted. And If it returns a data object, it'll replace the original payload.

#### 2. After Hook [#2-after-hook]

* **Purpose**: This hook is called after the respective entity is created or updated.
* **Behavior**: You can perform additional actions or modifications after the entity has been successfully created or updated.

**Example Usage**

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          // Modify the user object before it is created
          return {
            data: {
              // Ensure to return Better-Auth named fields, not the original field names in your database.
              ...user,
              firstName: user.name.split(" ")[0],
              lastName: user.name.split(" ")[1],
            },
          };
        },
        after: async (user) => {
          //perform additional actions, like creating a stripe customer
        },
      },
      delete: {
        before: async (user, ctx) => {
          console.log(`User ${user.email} is being deleted`);
          if (user.email.includes("admin")) {
            return false; // Abort deletion
          }

          return true; // Allow deletion
        },
        after: async (user) => {
          console.log(`User ${user.email} has been deleted`);
        },
      },
    },
    session: {
      delete: {
        before: async (session, ctx) => {
          console.log(`Session ${session.token} is being deleted`);
          if (session.userId === "admin-user-id") {
            return false; // Abort deletion
          }
          return true; // Allow deletion
        },
        after: async (session) => {
          console.log(`Session ${session.token} has been deleted`);
        },
      },
    },
  },
});
```

#### Throwing Errors [#throwing-errors]

If you want to stop the database hook from proceeding, you can throw errors using the `APIError` class imported from `better-auth/api`.

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (user.isAgreedToTerms === false) {
            // Your special condition.
            // Send the API error.
            throw new APIError("BAD_REQUEST", {
              message: "User must agree to the TOS before signing up.",
            });
          }
          return {
            data: user,
          };
        },
      },
    },
  },
});
```

#### Using the Context Object [#using-the-context-object]

The context object (`ctx`), passed as the second argument to the hook, contains useful information. For `update` hooks, this includes the current `session`, which you can use to access the logged-in user's details.

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      update: {
        before: async (data, ctx) => {
          // You can access the session from the context object.
          if (ctx.context.session) {
            console.log(
              "User update initiated by:",
              ctx.context.session.userId
            );
          }
          return { data };
        },
      },
    },
  },
});
```

Much like standard hooks, database hooks also provide a `ctx` object that offers a variety of useful properties. Learn more in the [Hooks Documentation](/docs/concepts/hooks#ctx).

## Plugins Schema [#plugins-schema]

Plugins can define their own tables in the database to store additional data. They can also add columns to the core tables to store additional data. For example, the two factor authentication plugin adds the following columns to the `user` table:

* `twoFactorEnabled`: Whether two factor authentication is enabled for the user.
* `twoFactorSecret`: The secret key used to generate TOTP codes.
* `twoFactorBackupCodes`: Encrypted backup codes for account recovery.

To add new tables and columns to your database, you have two options:

`CLI`: Use the migrate or generate command. These commands will scan your database and guide you through adding any missing tables or columns.
`Manual Method`: Follow the instructions in the plugin documentation to manually add tables and columns.

Both methods ensure your database schema stays up to date with your plugins' requirements.

## Joins [#joins]

Since Better-Auth version `1.4` we've introduced database joins support.
This allows Better-Auth to perform multiple database queries in a single request, reducing the number of database roundtrips.
Over 50 endpoints support joins, and we're constantly adding more.

Under the hood, our adapter system supports joins natively. When joins are disabled (the default),
Better-Auth falls back to making multiple database queries and combining the results.

To enable joins, update your auth config with the following:

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

export const auth = betterAuth({
  advanced: {
    database: {
      joins: true,
    },
  },
});
```

Make sure your DrizzleORM or PrismaORM schema includes the necessary relationships — run our migrate or generate CLI commands to stay up-to-date.

Read the documentation regarding joins for your given adapter:

* [DrizzleORM](/docs/adapters/drizzle#joins)
* [PrismaORM](/docs/adapters/prisma#joins)
* [SQLite](/docs/adapters/sqlite#joins)
* [MySQL](/docs/adapters/mysql#joins)
* [PostgreSQL](/docs/adapters/postgresql#joins)
* [MSSQL](/docs/adapters/mssql#joins)
* [MongoDB](/docs/adapters/mongo#joins)

