> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/better-auth/better-auth/llms.txt
> Use this file to discover all available pages before exploring further.

# Database

> Configure database adapters, run migrations, use secondary storage with Redis, understand the core schema, extend tables, and control ID generation.

Better Auth connects to a database to store users, sessions, accounts, and verification records. Plugins can also define their own tables to store additional data.

<Note>
  Better Auth can also run without any database. See [Stateless Session Management](/concepts/session-management#stateless-session-management) for details.
</Note>

## Adapters

Pass a supported database instance to the `database` option in your auth config. Better Auth has built-in support for SQLite, PostgreSQL, MySQL, and MSSQL via a Kysely adapter, plus first-class ORM adapters for Drizzle and Prisma.

Better Auth supports SQLite, PostgreSQL, MySQL, MSSQL, and more through the built-in Kysely adapter.

## CLI

Better Auth ships a CLI tool to manage migrations and generate schema files.

### Running migrations

The CLI checks your database and prompts you to add missing tables or update existing columns. This is only supported for the built-in Kysely adapter.

```bash theme={null}
npx auth@latest migrate
```

<Note>
  For PostgreSQL users: the migrate command supports non-default schemas. It automatically detects your `search_path` configuration and creates tables in the correct schema.
</Note>

### Generating schema

For ORM adapters like Prisma or Drizzle, use the `generate` command to produce the correct schema definition for your ORM. For the built-in Kysely adapter, this generates an SQL file you can run directly.

```bash theme={null}
npx auth@latest generate
```

### Programmatic migrations

In environments where the CLI is unavailable (such as Cloudflare Workers), run migrations programmatically:

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

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

await runMigrations();
```

<Warning>
  `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.
</Warning>

<AccordionGroup>
  <Accordion title="Example: Cloudflare D1">
    Cloudflare D1 can only be queried through a Cloudflare Worker, so the CLI cannot access it directly. Run migrations through a protected endpoint instead:

    ```typescript title="auth.ts" theme={null}
    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" theme={null}
    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;
    ```
  </Accordion>
</AccordionGroup>

## Secondary storage

Secondary storage lets you offload session data, verification records, and rate-limiting counters to a high-performance key-value store like Redis.

### Interface

Implement the `SecondaryStorage` interface:

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

Then pass your implementation to `betterAuth`:

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

export const auth = betterAuth({
  // ... other options
  secondaryStorage: {
    // your implementation
  },
});
```

### Official Redis package

Better Auth provides an official Redis storage package backed by [ioredis](https://github.com/redis/ioredis):

```bash theme={null}
npm install @better-auth/redis-storage ioredis
```

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

const redis = new Redis({
  host: "localhost",
  port: 6379,
});

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

The Redis storage supports standalone, cluster, and sentinel configurations.

### Manual Redis implementation

You can also implement secondary storage manually using the `redis` package:

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

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

export const auth = betterAuth({
  // ... other options
  secondaryStorage: {
    get: async (key) => {
      return await redis.get(key);
    },
    set: async (key, value, ttl) => {
      if (ttl) await redis.set(key, value, { EX: ttl });
      else await redis.set(key, value);
    },
    delete: async (key) => {
      await redis.del(key);
    },
  },
});
```

## Core schema

Better Auth requires four tables in your database. The types below use TypeScript notation — use the corresponding types for your database.

### User table

| Field           | Type                | Description                   |
| --------------- | ------------------- | ----------------------------- |
| `id`            | `string` (PK)       | Unique identifier             |
| `name`          | `string`            | Display name                  |
| `email`         | `string` (unique)   | Email address                 |
| `emailVerified` | `boolean`           | Whether the email is verified |
| `image`         | `string` (optional) | Avatar URL                    |
| `createdAt`     | `Date`              | Account creation timestamp    |
| `updatedAt`     | `Date`              | Last update timestamp         |

### Session table

| Field       | Type                | Description           |
| ----------- | ------------------- | --------------------- |
| `id`        | `string` (PK)       | Unique identifier     |
| `userId`    | `string` (FK)       | Reference to the user |
| `token`     | `string` (unique)   | Session token         |
| `expiresAt` | `Date`              | Expiry timestamp      |
| `ipAddress` | `string` (optional) | Client IP address     |
| `userAgent` | `string` (optional) | Client user agent     |
| `createdAt` | `Date`              | Creation timestamp    |
| `updatedAt` | `Date`              | Last update timestamp |

### Account table

| Field                   | Type                | Description                           |
| ----------------------- | ------------------- | ------------------------------------- |
| `id`                    | `string` (PK)       | Unique identifier                     |
| `userId`                | `string` (FK)       | Reference to the user                 |
| `accountId`             | `string`            | Provider-assigned account ID          |
| `providerId`            | `string`            | Authentication provider ID            |
| `accessToken`           | `string` (optional) | OAuth access token                    |
| `refreshToken`          | `string` (optional) | OAuth refresh token                   |
| `accessTokenExpiresAt`  | `Date` (optional)   | Access token expiry                   |
| `refreshTokenExpiresAt` | `Date` (optional)   | Refresh token expiry                  |
| `scope`                 | `string` (optional) | Granted OAuth scopes                  |
| `idToken`               | `string` (optional) | OIDC ID token                         |
| `password`              | `string` (optional) | Hashed password (email/password auth) |
| `createdAt`             | `Date`              | Creation timestamp                    |
| `updatedAt`             | `Date`              | Last update timestamp                 |

### Verification table

| Field        | Type          | Description                     |
| ------------ | ------------- | ------------------------------- |
| `id`         | `string` (PK) | Unique identifier               |
| `identifier` | `string`      | Verification request identifier |
| `value`      | `string`      | Value to verify                 |
| `expiresAt`  | `Date`        | Expiry timestamp                |
| `createdAt`  | `Date`        | Creation timestamp              |
| `updatedAt`  | `Date`        | Last update timestamp           |

## Custom tables

### Custom table and column names

Override table and column names using the `modelName` and `fields` properties:

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

<Note>
  Type inference in your code always uses the original field names (e.g., `user.name`, not `user.full_name`).
</Note>

To customize plugin table names, use the `schema` property inside the plugin config:

```typescript title="auth.ts" theme={null}
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 the core schema

Add custom fields to the `user` or `session` tables using `additionalFields`. The CLI will automatically update the database schema, and these fields will be properly inferred by TypeScript in functions like `useSession` and `signUp.email`.

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

export const auth = betterAuth({
  user: {
    additionalFields: {
      role: {
        type: ["user", "admin"],
        required: false,
        defaultValue: "user",
        input: false, // prevent user from setting this at signup
      },
      lang: {
        type: "string",
        required: false,
        defaultValue: "en",
      },
    },
  },
});
```

Field options:

* `type`: Data type — `"string"`, `"number"`, `"boolean"`, `"date"`, or a tuple of string literals for enums.
* `required`: Whether the field is required on create.
* `defaultValue`: Default value (JS layer only; the column is optional in the database).
* `input`: When `false`, the field cannot be provided by the user at signup (useful for fields like `role`).

Accessing additional fields:

```typescript theme={null}
const res = await auth.api.signUpEmail({
  body: {
    email: "test@example.com",
    password: "password",
    name: "John Doe",
    lang: "fr",
  },
});

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

### Mapping OAuth profiles to additional fields

Use `mapProfileToUser` to populate additional user fields from an OAuth provider's profile:

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

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

## ID generation

Better Auth generates unique string IDs for all entities by default. You can customize this behavior with `advanced.database.generateId`.

### Let the database generate IDs

Set `generateId` to `false` to let your database handle all ID generation:

```typescript title="auth.ts" theme={null}
export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: false,
    },
  },
});
```

### Custom ID generation function

Return `false` or `undefined` from the function to let the database generate IDs for specific models:

```typescript title="auth.ts" theme={null}
export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: (options) => {
        if (options.model === "user" || options.model === "users") {
          return false; // database generates IDs for users
        }
        return crypto.randomUUID();
      },
    },
  },
});
```

### Auto-incrementing numeric IDs

Set `generateId` to `"serial"` for auto-incrementing numeric IDs across all tables. The CLI will generate the schema with the `id` field as a numeric type.

```typescript title="auth.ts" theme={null}
export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: "serial",
    },
  },
});
```

<Note>
  Better Auth infers `id` as a `string` internally but converts to and from the numeric type when reading or writing to the database. Always pass IDs as strings to Better Auth endpoints.
</Note>

### UUIDs

Set `generateId` to `"uuid"` to use UUID columns in your database. For PostgreSQL, Better Auth allows the database to generate the UUID automatically.

## Database hooks

Database hooks execute custom logic before or after core database operations on the **user**, **session**, and **account** models.

### Hook types

* **Before hook**: Runs before create, update, or delete. Return `false` to abort the operation. Return a data object to replace the original payload.
* **After hook**: Runs after create or update. Use for side effects like sending notifications or provisioning external resources.

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          return {
            data: {
              ...user,
              firstName: user.name.split(" ")[0],
              lastName: user.name.split(" ")[1],
            },
          };
        },
        after: async (user) => {
          // e.g., create a Stripe customer
        },
      },
      delete: {
        before: async (user, ctx) => {
          if (user.email.includes("admin")) {
            return false; // abort deletion
          }
          return true;
        },
        after: async (user) => {
          console.log(`User ${user.email} has been deleted`);
        },
      },
    },
    session: {
      delete: {
        before: async (session, ctx) => {
          if (session.userId === "admin-user-id") {
            return false;
          }
          return true;
        },
      },
    },
  },
});
```

### Throwing errors from hooks

Use `APIError` to abort an operation and return an error response:

```typescript title="auth.ts" theme={null}
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) {
            throw new APIError("BAD_REQUEST", {
              message: "User must agree to the TOS before signing up.",
            });
          }
          return { data: user };
        },
      },
    },
  },
});
```

### Using the context object

The `ctx` object (second argument) carries useful information. For `update` hooks, it includes the current `session`:

```typescript title="auth.ts" theme={null}
export const auth = betterAuth({
  databaseHooks: {
    user: {
      update: {
        before: async (data, ctx) => {
          if (ctx.context.session) {
            console.log("Update initiated by:", ctx.context.session.userId);
          }
          return { data };
        },
      },
    },
  },
});
```

## Plugin schema

Plugins can define their own tables and add columns to core tables. For example, the two-factor authentication plugin adds `twoFactorEnabled`, `twoFactorSecret`, and `twoFactorBackupCodes` columns to the `user` table.

To update your schema after adding a plugin, run `npx auth@latest migrate` or `npx auth@latest generate`.

## Experimental joins

Since Better Auth v1.4, you can enable experimental database joins to reduce the number of database roundtrips. Over 50 endpoints support joins.

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

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

<Warning>
  After enabling joins, run `migrate` or `generate` to update your ORM schema with the required relationship definitions.
</Warning>

Read the adapter-specific joins documentation before enabling:
[Drizzle](/adapters/drizzle) · [Prisma](/adapters/prisma) · [SQLite](/adapters/sqlite) · [PostgreSQL](/adapters/postgresql)
