Database
Database adapters for storing users, sessions, and accounts.
Better Auth uses the DatabaseAdapter trait to abstract storage. Two adapters are provided: in-memory and PostgreSQL. Both support custom entity types via generic type parameters.
MemoryDatabaseAdapter
An in-memory adapter for development and testing. Data is lost when the process exits.
use better_auth::adapters::MemoryDatabaseAdapter;
// Using built-in types:
let database = MemoryDatabaseAdapter::new();No configuration needed. Thread-safe via Arc<Mutex<...>>.
Custom Entity Types (Memory)
Use a type alias with your custom structs:
use better_auth::adapters::MemoryDatabaseAdapter;
type AppDb = MemoryDatabaseAdapter<
AppUser, AppSession, AppAccount,
AppOrg, AppMember, AppInvitation, AppVerification,
>;
let database = AppDb::default();Custom structs need Auth* and Memory* derive macros. See Custom Entity Types below.
PostgreSQL (SqlxAdapter)
Requires the sqlx-postgres feature flag:
[dependencies]
better-auth = { version = "0.6", features = ["sqlx-postgres"] }use better_auth::adapters::SqlxAdapter;
let database = SqlxAdapter::new("postgresql://user:pass@localhost:5432/mydb").await?;Connection Pool Options
use better_auth::adapters::{SqlxAdapter, PoolConfig};
let config = PoolConfig {
max_connections: 10,
min_connections: 0,
acquire_timeout: std::time::Duration::from_secs(30),
idle_timeout: Some(std::time::Duration::from_secs(600)),
max_lifetime: Some(std::time::Duration::from_secs(1800)),
};
let database = SqlxAdapter::with_config("postgresql://...", config).await?;| Field | Default | Description |
|---|---|---|
max_connections | 10 | Maximum pool size |
min_connections | 0 | Minimum idle connections |
acquire_timeout | 30s | Timeout for acquiring a connection |
idle_timeout | 600s | Close idle connections after this duration |
max_lifetime | 1800s | Maximum connection lifetime |
Custom Entity Types (PostgreSQL)
Use a type alias with your custom structs:
use better_auth::adapters::{SqlxAdapter, PoolConfig};
type AppDb = SqlxAdapter<
AppUser, AppSession, AppAccount,
AppOrg, AppMember, AppInvitation, AppVerification,
>;
let pool = sqlx::PgPool::connect("postgresql://...").await?;
let database = AppDb::from_pool(pool);Custom structs need Auth* derives plus sqlx::FromRow:
#[derive(Clone, Debug, Serialize, sqlx::FromRow, AuthUser)]
struct AppUser {
id: String,
email: Option<String>,
name: Option<String>,
// ... standard fields ...
// Custom fields (must have DB column defaults):
plan: String,
stripe_customer_id: Option<String>,
}The adapter uses SELECT * and RETURNING *, so custom columns in your database are automatically populated via FromRow. Custom columns not in the INSERT get their database-level defaults.
Schema
Run the initial migration to create the required tables:
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT,
email TEXT UNIQUE,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
image TEXT,
username TEXT UNIQUE,
display_username TEXT,
two_factor_enabled BOOLEAN NOT NULL DEFAULT FALSE,
role TEXT,
banned BOOLEAN NOT NULL DEFAULT FALSE,
ban_reason TEXT,
ban_expires TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
expires_at TIMESTAMPTZ NOT NULL,
token TEXT NOT NULL UNIQUE,
ip_address TEXT,
user_agent TEXT,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
impersonated_by TEXT,
active_organization_id TEXT,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
access_token TEXT,
refresh_token TEXT,
id_token TEXT,
access_token_expires_at TIMESTAMPTZ,
refresh_token_expires_at TIMESTAMPTZ,
scope TEXT,
password TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(provider_id, account_id)
);
CREATE TABLE IF NOT EXISTS verifications (
id TEXT PRIMARY KEY,
identifier TEXT NOT NULL,
value TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Organization tables (created by the organization migration):
CREATE TABLE IF NOT EXISTS organization (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
logo TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS member (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
CREATE TABLE IF NOT EXISTS invitation (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
status TEXT NOT NULL DEFAULT 'pending',
inviter_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Docker Quick Start
docker run --name better-auth-postgres \
-e POSTGRES_DB=better_auth \
-e POSTGRES_USER=better_auth \
-e POSTGRES_PASSWORD=password \
-p 5432:5432 \
-d postgres:15Custom Entity Types
Both adapters support custom entity types via generic type parameters. This lets you add business-specific fields (like plan, stripe_customer_id, etc.) while reusing the framework's adapter logic.
Derive Macros
Enable the derive feature:
[dependencies]
better-auth = { version = "0.6", features = ["derive"] }Each entity needs two derive macros:
| Entity | Read-only trait (getters) | Memory adapter | PostgreSQL |
|---|---|---|---|
| User | AuthUser | MemoryUser | sqlx::FromRow |
| Session | AuthSession | MemorySession | sqlx::FromRow |
| Account | AuthAccount | MemoryAccount | sqlx::FromRow |
| Organization | AuthOrganization | MemoryOrganization | sqlx::FromRow |
| Member | AuthMember | MemoryMember | sqlx::FromRow |
| Invitation | AuthInvitation | MemoryInvitation | sqlx::FromRow |
| Verification | AuthVerification | MemoryVerification | sqlx::FromRow |
Example
use better_auth_core::{AuthUser, MemoryUser};
#[derive(Clone, Debug, Serialize, AuthUser, MemoryUser)]
struct AppUser {
// --- Required fields (same as built-in User) ---
id: String,
email: Option<String>,
#[auth(field = "name")] // remap: getter is .name(), field is display_name
display_name: Option<String>,
email_verified: bool,
image: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
username: Option<String>,
display_username: Option<String>,
two_factor_enabled: bool,
role: Option<String>,
banned: bool,
ban_reason: Option<String>,
ban_expires: Option<DateTime<Utc>>,
metadata: serde_json::Value,
// --- Custom fields ---
#[auth(default = r#""free".to_string()"#)] // default for Memory adapter
plan: String,
stripe_customer_id: Option<String>, // defaults to None
}Attributes
#[auth(field = "name")]— Remap a struct field to a different getter name. The derive generates.name()instead of.display_name().#[auth(default = "expr")]— Default value expression for the Memory adapter'sfrom_create(). Custom fields without this attribute useDefault::default().
Full Example
See examples/custom_entities.rs for a complete working example with all 7 entity types, both Auth* and Memory* derives, and a full auth flow.
cargo run --example custom_entities --features deriveThe DatabaseAdapter Trait
The trait uses associated types so adapters can return custom entity structs:
#[async_trait]
pub trait DatabaseAdapter: Send + Sync + 'static {
type User: AuthUser;
type Session: AuthSession;
type Account: AuthAccount;
type Organization: AuthOrganization;
type Member: AuthMember;
type Invitation: AuthInvitation;
type Verification: AuthVerification;
// User operations
async fn create_user(&self, user: CreateUser) -> AuthResult<Self::User>;
async fn get_user_by_id(&self, id: &str) -> AuthResult<Option<Self::User>>;
async fn get_user_by_email(&self, email: &str) -> AuthResult<Option<Self::User>>;
async fn update_user(&self, id: &str, update: UpdateUser) -> AuthResult<Self::User>;
async fn delete_user(&self, id: &str) -> AuthResult<()>;
// Session operations
async fn create_session(&self, session: CreateSession) -> AuthResult<Self::Session>;
async fn get_session(&self, token: &str) -> AuthResult<Option<Self::Session>>;
// ... and more
// Organization, Member, Invitation operations
async fn create_organization(&self, org: CreateOrganization) -> AuthResult<Self::Organization>;
async fn create_member(&self, member: CreateMember) -> AuthResult<Self::Member>;
async fn create_invitation(&self, inv: CreateInvitation) -> AuthResult<Self::Invitation>;
// ... and more
}Implement this trait to use a custom storage backend. See the built-in MemoryDatabaseAdapter and SqlxAdapter implementations for reference.