SQLx
SQLx is the default store backend. SqlxStore supports SQLite and PostgreSQL and works with the generated models or your own AuthEntity models.
Dependencies
Section titled “Dependencies”SQLx is enabled by the default sqlx feature. To pick one engine, disable default features and select sqlx-sqlite or sqlx-postgres; keep a TLS feature (native-tls or rustls) for outbound HTTP:
alibi = { version = "0.1.1", default-features = false, features = ["axum", "sqlx-postgres", "rustls"] }sqlx = { version = "0.9", default-features = false, features = ["postgres", "chrono", "json", "derive"] }The generated models derive from sqlx::FromRow, so your application depends on sqlx with the chrono, json and derive features and the engine you use. See Cargo features.
Connect and build
Section titled “Connect and build”SqlxPool::connect selects the engine from the URL scheme (sqlite: or postgres: / postgresql:):
use crate::auth_schema::{AppAuthSchema, run_app_migrations};use alibi::plugins::EmailPasswordPlugin;use alibi::sqlx::{SqlxPool, SqlxStore};use alibi::{AuthConfig, BetterAuth};
async fn build_auth( secret: &str, database_url: &str,) -> Result<BetterAuth<AppAuthSchema>, Box<dyn std::error::Error>> { let config = AuthConfig::new(secret).base_url("http://localhost:3000"); let pool = SqlxPool::connect(database_url).await?; run_app_migrations(&pool).await?; let store = SqlxStore::<AppAuthSchema>::new(config.clone(), pool);
Ok(BetterAuth::<AppAuthSchema>::new(config) .store(store) .plugin(EmailPasswordPlugin::new().enable_signup(true)) .build() .await?)}Always pass the same AuthConfig to the store and the builder: the store applies field policies and session settings from it.
Share your application’s pool
Section titled “Share your application’s pool”Better Auth runs on the pool you give it, so you can tune connection limits and share one pool between your application and authentication. Wrap an existing sqlx pool with SqlxPool::from and read it back with as_postgres() / as_sqlite():
use alibi::sqlx::SqlxPool;use sqlx::postgres::PgPoolOptions;
async fn shared_pool(url: &str) -> Result<(SqlxPool, sqlx::PgPool), sqlx::Error> { let pg = PgPoolOptions::new() .max_connections(20) .acquire_timeout(std::time::Duration::from_secs(5)) .connect(url) .await?; let auth_pool = SqlxPool::from(pg.clone()); // pools are reference-counted handles Ok((auth_pool, pg))}Auth writes that must be atomic (sign-up, session issuance, organization changes) run in transactions on this pool.
For organization plugin tables, bind three application models with OrganizationModels; see application-owned organization tables.
Use your own models
Section titled “Use your own models”Derive sqlx::FromRow and alibi::sqlx::AuthEntity on structs that contain the required fields for their role (user, session, account, verification). Extra columns are allowed:
#[derive(Clone, Debug, serde::Serialize, sqlx::FromRow, alibi::sqlx::AuthEntity)]#[auth(role = "session", table = "app_sessions")]pub struct Session { pub id: String, pub expires_at: chrono::DateTime<chrono::Utc>, pub token: String, pub created_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>, pub ip_address: Option<String>, pub user_agent: Option<String>, pub user_id: String, pub active: bool,}| Attribute | Where | Meaning |
|---|---|---|
#[auth(role = "…")] |
struct | user, session, account or verification |
#[auth(table = "…")] |
struct | Table name (default users, sessions, accounts, verifications) |
#[auth(id_generator = "path::to::fn")] |
struct | Application ID factory returning a String |
#[auth(secondary_storage)] |
struct | Allow secondary-storage snapshots |
#[sqlx(rename = "…")] |
field | Column name |
#[auth(column_type = "bpchar")] |
field | PostgreSQL wire type for fixed-width CHAR(n) columns |
Select the models in an AuthSchema (the generated file does this for you):
pub struct AppAuthSchema;impl alibi::AuthSchema for AppAuthSchema { type User = user::Model; type Session = session::Model; type Account = account::Model; type Verification = verification::Model;}Timestamps: use DateTime<Utc> for SQLite and PostgreSQL TIMESTAMPTZ; use NaiveDateTime for an existing PostgreSQL TIMESTAMP column that already stores UTC. See Existing databases.
Wrap the store to observe or veto writes. In the hook, ctx.db is the pool and ctx.tx the active transaction:
use crate::auth_schema::AppAuthSchema;use async_trait::async_trait;use alibi::sqlx::{SqlxBackend, SqlxStore};use alibi::store::{DatabaseHookContext, DatabaseHooks, HookControl};use alibi::prelude::{AuthUser, CreateUser};use alibi::AuthResult;
struct RejectReserved;
#[async_trait]impl DatabaseHooks<AppAuthSchema, SqlxBackend> for RejectReserved { async fn before_create_user( &self, user: &mut CreateUser, _: &DatabaseHookContext<'_, SqlxBackend>, ) -> AuthResult<HookControl> { let reserved = user.email.as_deref().is_some_and(|email| email.starts_with("admin@")); Ok(if reserved { HookControl::Cancel } else { HookControl::Continue }) }}
fn hooked(store: SqlxStore<AppAuthSchema>) -> SqlxStore<AppAuthSchema> { store.hook(RejectReserved)}More in Hooks.
Migrations
Section titled “Migrations”run_app_migrations(&pool) creates the generated tables once. For real deployments use versioned migrations and consider the library’s bundled schema (SchemaMigrator::migrate) or its reference DDL as a source. Plugin tables (two-factor, passkey, API keys, JWKS, organizations, …) are included in the generated schema when you pass --plugins.
SQLite note: SQLite has no native TIMESTAMPTZ; timestamps are stored as UTC text and compared accordingly.
Frontend
Section titled “Frontend”See the official database guide.