Better Auth in Rust

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:

Cargo.toml
[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?;
FieldDefaultDescription
max_connections10Maximum pool size
min_connections0Minimum idle connections
acquire_timeout30sTimeout for acquiring a connection
idle_timeout600sClose idle connections after this duration
max_lifetime1800sMaximum 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:

migrations/001_initial.sql
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):

migrations/002_organization.sql
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:15

Custom 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:

Cargo.toml
[dependencies]
better-auth = { version = "0.6", features = ["derive"] }

Each entity needs two derive macros:

EntityRead-only trait (getters)Memory adapterPostgreSQL
UserAuthUserMemoryUsersqlx::FromRow
SessionAuthSessionMemorySessionsqlx::FromRow
AccountAuthAccountMemoryAccountsqlx::FromRow
OrganizationAuthOrganizationMemoryOrganizationsqlx::FromRow
MemberAuthMemberMemoryMembersqlx::FromRow
InvitationAuthInvitationMemoryInvitationsqlx::FromRow
VerificationAuthVerificationMemoryVerificationsqlx::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's from_create(). Custom fields without this attribute use Default::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 derive

The 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.

On this page