docs/content/docs/concepts/database.mdx
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 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>Better Auth comes with a CLI tool to manage database migrations and generate schema.
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.
npx auth@latest migrate
Better Auth also provides a generate command to generate the schema required by Better Auth. The generate command creates 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.
npx auth@latest generate
See the 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>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.
import { getMigrations } from "better-auth/db/migration";
import { auth } from "./auth";
const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(auth.options);
await runMigrations();
```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>
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.
To use secondary storage, implement the SecondaryStorage interface:
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:
import { betterAuth } from "better-auth";
betterAuth({
// ... other options
secondaryStorage: {
// Your implementation here
},
});
Better Auth provides an official Redis storage package that uses ioredis:
npm install @better-auth/redis-storage ioredis
# or
pnpm add @better-auth/redis-storage ioredis
Usage:
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:
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:
npm install @upstash/redis
Then, create a new Redis client:
// 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 for more information on how to use Upstash Redis with Node.js.
After that, we can create a secondary storage implementation:
// 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.
import { betterAuth } from "better-auth";
import { redisSecondaryStorage } from "~/lib/auth/adapters/redis-secondary-storage";
export const auth = betterAuth({
// ... other options
secondaryStorage: redisSecondaryStorage,
});
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.
Table Name: user
export const userTableFields = [ { name: "id", type: "string", description: "Unique identifier for each user", isPrimaryKey: true, }, { name: "name", type: "string", description: "User's chosen display name", }, { name: "email", type: "string", description: "User's email address for communication and login", isUnique: true, }, { name: "emailVerified", type: "boolean", description: "Whether the user's email is verified", }, { name: "image", type: "string", description: "User's image url", isOptional: true, }, { name: "createdAt", type: "Date", description: "Timestamp of when the user account was created", }, { name: "updatedAt", type: "Date", description: "Timestamp of the last update to the user's information", }, ];
<DatabaseTable name="user" fields={userTableFields} />Table Name: session
export const sessionTableFields = [ { name: "id", type: "string", description: "Unique identifier for each session", isPrimaryKey: true, }, { name: "userId", type: "string", description: "The ID of the user", isForeignKey: true, references: { model: "user", field: "id", onDelete: "cascade" }, }, { name: "token", type: "string", description: "The unique session token", isUnique: true, }, { name: "expiresAt", type: "Date", description: "The time when the session expires", }, { name: "ipAddress", type: "string", description: "The IP address of the device", isOptional: true, }, { name: "userAgent", type: "string", description: "The user agent information of the device", isOptional: true, }, { name: "createdAt", type: "Date", description: "Timestamp of when the session was created", }, { name: "updatedAt", type: "Date", description: "Timestamp of when the session was updated", }, ];
<DatabaseTable name="session" fields={sessionTableFields} />Table Name: account
export const accountTableFields = [ { name: "id", type: "string", description: "Unique identifier for each account", isPrimaryKey: true, }, { name: "userId", type: "string", description: "The ID of the user", isForeignKey: true, references: { model: "user", field: "id", onDelete: "cascade" }, }, { name: "accountId", type: "string", description: "The ID of the account as provided by the SSO or equal to userId for credential accounts", }, { name: "providerId", type: "string", description: "The ID of the provider", }, { name: "accessToken", type: "string", description: "The access token of the account. Returned by the provider", isOptional: true, }, { name: "refreshToken", type: "string", description: "The refresh token of the account. Returned by the provider", isOptional: true, }, { name: "accessTokenExpiresAt", type: "Date", description: "The time when the access token expires", isOptional: true, }, { name: "refreshTokenExpiresAt", type: "Date", description: "The time when the refresh token expires", isOptional: true, }, { name: "scope", type: "string", description: "The scope of the account. Returned by the provider", isOptional: true, }, { name: "idToken", type: "string", description: "The ID token returned from the provider", isOptional: true, }, { name: "password", type: "string", description: "The password of the account. Mainly used for email and password authentication", isOptional: true, }, { name: "createdAt", type: "Date", description: "Timestamp of when the account was created", }, { name: "updatedAt", type: "Date", description: "Timestamp of when the account was updated", }, ];
<DatabaseTable name="account" fields={accountTableFields} />Table Name: verification
export const verificationTableFields = [ { name: "id", type: "string", description: "Unique identifier for each verification", isPrimaryKey: true, }, { name: "identifier", type: "string", description: "The identifier for the verification request", }, { name: "value", type: "string", description: "The value to be verified", }, { name: "expiresAt", type: "Date", description: "The time when the verification request expires", }, { name: "createdAt", type: "Date", description: "Timestamp of when the verification request was created", }, { name: "updatedAt", type: "Date", description: "Timestamp of when the verification request was updated", }, ];
<DatabaseTable name="verification" fields={verificationTableFields} />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.
You can customize the table names and column names for the core schema by using the modelName and fields properties in your auth config:
export const auth = betterAuth({
user: {
modelName: "users",
fields: {
name: "full_name",
email: "email_address",
},
},
session: {
modelName: "user_sessions",
fields: {
userId: "user_id",
},
},
});
To customize table names and column name for plugins, you can use the schema property in the plugin config:
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",
},
},
},
}),
],
});
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: This determines whether a value can be provided when creating a new record (default: true). If there are additional fields, like role, that should not be provided by the user during signup, you can set this to false.Here's an example of how to extend the user schema with additional fields:
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.
//on signup
const res = await auth.api.signUpEmail({
body: {
email: '[email protected]',
password: 'password',
name: 'John Doe',
lang: 'fr',
},
});
//user object
res.user.role; // > "admin"
res.user.lang; // > "fr"
If you're using social / OAuth providers, you may want to provide mapProfileToUser to map the profile data to the user object. So, you can populate additional fields from the provider's profile.
Example: Mapping Profile to User For firstName and lastName
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,
};
},
},
},
});
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.
Setting generateId to false allows your database handle all ID generation: (outside of generateId being serial and some cases of generateId being uuid)
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
},
},
});
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:
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();
},
},
},
});
Generate the same type of ID for all tables:
import { betterAuth } from "better-auth";
import { db } from "./db";
export const auth = betterAuth({
database: db,
advanced: {
database: {
generateId: () => crypto.randomUUID(),
},
},
});
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.
import { betterAuth } from "better-auth";
import { db } from "./db";
export const auth = betterAuth({
database: db,
advanced: {
database: {
generateId: "serial",
},
},
});
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>
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.
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.
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:
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:
false, the operation will be aborted. And If it returns a data object, it'll replace the original payload.Example Usage
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`);
},
},
},
},
});
If you want to stop the database hook from proceeding, you can throw errors using the APIError class imported from better-auth/api.
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,
};
},
},
},
},
});
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.
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.
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.
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:
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: