Database
SeaORM-backed database integration for Better Auth.
Better Auth uses your application's SeaORM entities and DatabaseConnection
for auth persistence through the seaorm2 feature on better-auth.
The supported public path is:
- Enable the
seaorm2feature onbetter-auth. - Define auth entities in your app with
#[derive(AuthEntity)]frombetter_auth::seaorm. - Register them in an app schema with
#[derive(AuthSchema)]. - Manage auth migrations in your own SeaORM migrator.
- Build a
SeaOrmStorefrom the sharedDatabaseConnectionand pass it toBetterAuth::<YourSchema>::new(config).store(...).
Setup
Generate src/auth_schema.rs with the quick-start guide first.
mod auth_schema;
use auth_schema::{AppAuthSchema, create_auth_tables};
use better_auth::{AuthConfig, BetterAuth};
use better_auth::plugins::EmailPasswordPlugin;
use better_auth::seaorm::{Database, DatabaseConnection, SeaOrmStore};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database = Database::connect("sqlite::memory:").await?;
create_auth_tables(&database).await?;
let auth_database: DatabaseConnection = database.clone();
let config = AuthConfig::new("your-very-secure-secret-key-at-least-32-chars-long")
.base_url("http://localhost:3000");
let store = SeaOrmStore::<AppAuthSchema>::new(config.clone(), auth_database);
let auth = BetterAuth::<AppAuthSchema>::new(
config,
)
.store(store)
.plugin(EmailPasswordPlugin::new().enable_signup(true))
.build()
.await?;
// Keep using `database` for your app's own tables and queries.
println!("plugins: {:?}", auth.plugin_names());
Ok(())
}Generate an initial schema
Run the CLI with every plugin that your application registers:
better-auth-rs generate --plugins two-factor,api-key --output src/auth_schema.rsThe CLI generates core entities, plugin fields and tables, unique indexes, and foreign keys. Unknown plugin names fail. Existing output files are preserved unless you pass --force.
Call the generated create_auth_tables(&database).await? from the application's initial migration. The function creates tables in an empty database and fails when a table already exists. The function does not compare schemas, alter existing tables, or track migration versions. Use your application's versioned SeaORM migrations for later changes, including plugin additions.
The application owns the generated source. The four core entities can be customized through the public schema traits. Plugin tables currently use fixed library entity names and columns; preserve their generated database schema.
For application-owned entities, bind the four model types with AuthSchema:
#[derive(better_auth::AuthSchema)]
#[auth(
user = "user::Model",
session = "session::Model",
account = "account::Model",
verification = "verification::Model"
)]
pub struct AppAuthSchema;Each model must implement its corresponding auth entity trait. You may place each binding in a separate #[auth(...)] attribute. Each binding must appear exactly once.
Auth responses serialize timestamps as UTC ISO 8601 strings with three fractional digits, matching the upstream JavaScript client. Stored database timestamps retain their original precision.
Plugin Fields
build().await rejects a plugin when the user or session entity cannot persist its required fields. AuthEntity derives this capability list from the model fields.
| Plugin | Required user fields | Required session fields |
|---|---|---|
Username, enabled with EmailPasswordPlugin::username(true) | username, display_username | None |
| Two-factor | two_factor_enabled | None |
| Admin | role, banned, ban_reason, ban_expires | impersonated_by |
| Organization | None | active_organization_id |
For manual AuthUser and AuthSession implementations, declare const PLUGIN_FIELDS: &'static [&'static str] with the supported Rust field names. Declare a field only when the entity getter and store writes support that field. The default empty list supports core fields only.
Run the application database migrations before starting the server. Capability validation checks Rust entities; capability validation does not inspect database tables or replace migrations.
Existing Databases
If your application already has a users table with extra fields or non-string primary keys, keep the same public integration story:
- Reuse your app-owned SeaORM entities.
- Implement the public
AuthUser/SeaOrmUserModeltraits manually instead of relying on#[derive(AuthEntity)]. - Keep
sessions,accounts, andverificationsas app-owned SeaORM entities in the same database. - Run your own SeaORM migrations alongside the rest of the application schema.
The canonical worked example is examples/postgres_usage.rs, which demonstrates:
- an existing app-owned schema
- numeric user IDs
- seeded legacy users
- additional required app fields on the user table
- Better Auth mounted on top of the same
DatabaseConnection
Testing
Run devenv shell -- ./scripts/consumer-check.sh to generate a fresh schema and test an independent consumer against the public package. The check compiles combined and separate AuthSchema attributes, creates SQLite tables, registers and signs in a user, protects an Axum route, creates an API key, and enables TOTP. Database constraint checks cover uniqueness and foreign keys.
Internal behavior tests also use a bundled schema. Dual-server and client SDK compatibility checks compare behavior with the TypeScript reference server.
Notes
- Custom stores must report uniqueness conflicts as
DatabaseError::UniqueConstraint. Device code generation retries only that error, up to three attempts. - Better Auth no longer owns a public SeaORM migration path. Auth and app tables should be migrated together by the host application.
MemoryCacheAdapterand the optional asynchronousRedisAdapterare standalone cache utilities. Neither adapter is connected to session persistence.RedisAdapter::clearclears the selected Redis database.