> You are reading Better Auth documentation for `v1.6`. This is not the current stable release. APIs may differ from the latest stable version.

# Database (/docs/1.6/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/1.6/adapters/other-relational-databases) documentation.

<Callout type="info">
  Better Auth also works without any database. For more details, see [Stateless Session Management](/docs/1.6/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/1.6/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/1.6/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>

## 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>;
  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]

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

```bash
npm install @better-auth/redis-storage ioredis
# or
pnpm add @better-auth/redis-storage ioredis
```

**Usage:**

```typescript
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 all ioredis connection modes including standalone, cluster, and sentinel configurations.

**Manual Implementation:**

If you prefer to implement your own Redis secondary storage, here's a basic example:

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

This implementation allows Better Auth to use Redis for storing session data and rate limiting counters. You can also add prefixes to the keys names.

**Example: Upstash Redis Implementation**

Here's an example using Upstash Redis. First, install the Upstash Redis client:

```bash
npm install @upstash/redis
```

Then, create a new Redis client:

```typescript
// src/lib/redis/index.ts

import { Redis } from "@upstash/redis";

export const redis = Redis.fromEnv();
```

Don't forget to set the `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` environment variables. Also, [see the Upstash documentation](https://upstash.com/docs/redis/howto/connectwithupstashredis) for more information on how to use Upstash Redis with Node.js.

After that, we can create a secondary storage implementation:

```typescript
// src/lib/auth/adapters/redis-secondary-storage.ts

import { SecondaryStorage } from "better-auth";
import { redis } from "~/lib/redis";

export const redisSecondaryStorage: SecondaryStorage = {
  async get(key: string) {
    try {
      const value = await redis.get(key);

      // Handle different return types from Redis
      if (value === null || value === undefined) {
        return null;
      }

      // If it's already a string, return it
      if (typeof value === "string") {
        return value;
      }

      // If it's an object, stringify it
      if (typeof value === "object") {
        return JSON.stringify(value);
      }

      // Convert to string for any other type
      return String(value);
    } catch (error) {
      console.error("Redis get error:", error);
      return null;
    }
  },

  async set(key: string, value: string, ttl?: number) {
    try {
      // Ensure value is a string
      const stringValue =
        typeof value === "string" ? value : JSON.stringify(value);

      if (ttl) {
        // Set with TTL in seconds
        await redis.set(key, stringValue, { ex: ttl });
      } else {
        // Set without TTL
        await redis.set(key, stringValue);
      }
    } catch (error) {
      console.error("Redis set error:", error);
      throw error;
    }
  },

  async delete(key: string) {
    try {
      await redis.del(key);
    } catch (error) {
      console.error("Redis delete error:", error);
      throw error;
    }
  },
};
```

Finally, we can pass the implementation to the `betterAuth` function.

```typescript
import { betterAuth } from "better-auth";
import { redisSecondaryStorage } from "~/lib/auth/adapters/redis-secondary-storage";

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

## 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`



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

### 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/1.6/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/1.6/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/1.6/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.

## Experimental Joins [#experimental-joins]

Since Better-Auth version `1.4` we've introduced experimental 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, meaning even if you don't enable experimental joins,
it will still fallback 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({
  experimental: { joins: true }
});
```

The Better-Auth `1.4` CLI will generate DrizzleORM and PrismaORM relationships for you so if you do not have those already
be sure to update your schema by running our migrate or generate CLI commands to be up-to-date with the latest required schema.

It's very important to read the documentation regarding experimental joins for your given adapter:

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

