storage/sqlite is a pure Go (modernc.org/sqlite, no cgo) adapter that runs on a *sql.DB you already own. It is its own Go module, so the SQLite driver is only pulled in when you import it.
What it covers
Set it as
Config.CoreStorage. Features that need a missing capability fail at New instead of at request time.
Open the database
You open the database and own its pragmas. Foreign keys must be on for every connection, andNew refuses a database where they are off (pass AllowForeignKeysOff to override, at the cost of ON DELETE CASCADE). Use WAL and a busy timeout when anything else writes to the file:
Ping serially at startup: switching a fresh file to WAL needs an exclusive lock that parallel first connections can lose with SQLITE_BUSY. _txlock=immediate makes every transaction take the write lock up front, which avoids SQLITE_BUSY on read-then-write upgrades under contention.
Standalone: let the package migrate
Migrate records applied versions in theauth_schema_migrations. Each step runs under BEGIN IMMEDIATE together with its ledger row, so replicas starting at once serialize and a failed step leaves nothing half applied.
Hosted: fold the SQL into your own migrator
Migrations() returns an fs.FS of numbered, forward-only .sql files (0001_core.sql, 0002_oauth_accounts.sql, …). Copy or embed them into your own numbered history, keeping their order, and do not call Migrate:
RenderMigrations(sqlitestore.WithTablePrefix("auth_")) returns the same steps with the prefix applied. Never edit a step you have already shipped; later versions of this package only add files.
Table prefix
Tables default to thetheauth_ prefix so they cannot collide with host tables. Pass the same WithTablePrefix option to Migrate, RenderMigrations and New. A prefix must match [A-Za-z_][A-Za-z0-9_]*.
Expiry sweeps
Expired rows are not removed on read. CallSweepExpired from a host ticker:
Importing existing data in one transaction
sqlite.NewTx(tx) binds a Store to a transaction you own, so a backfill of legacy users, API tokens, passkeys and TOTP secrets commits or rolls back as one unit. This works even when your *sql.DB has SetMaxOpenConns(1), because the Store never asks the pool for a second connection. Methods that need several statements use SAVEPOINTs inside your transaction instead of starting their own.
- Build a short-lived Store with
NewTxand use the package-levelImport*Tohelpers. Do not build aTheAuthon it: aTheAuthoutlives the transaction.(*TheAuth).ImportUser,ImportTOTPSecret,ImportWebAuthnCredentialandImportAPITokenexist for the case where you import through a live instance. ImportTOTPSecretTotakes the plaintext base32 secret and encrypts it with the same key as enrollment, so passConfig.EncryptionKey. The secret is never logged.- Recovery codes are not importable. Their hashes are salted by the library, so imported users must regenerate them.
- Password hashes are stored verbatim. Bcrypt hashes verify only when
PasswordPolicy.AllowLegacyBcryptis set. - Every helper validates input and returns
ErrImportInvalid(wrapped) for bad data andErrImportDuplicatewhen the record exists, so a re-run can skip what is already imported. Migratetakes a*sql.DBand cannot run inside a transaction.SweepExpiredworks on a transaction-bound Store but belongs on a normal Store after the commit. Stop using the Store once the transaction ends.
Shared login throttle
store.ThrottleStore() returns a LoginThrottleStore backed by the same database, so several processes share failure counters. It also implements LoginThrottleCASStore: the limiter re-reads and retries when another process changed an entry first, instead of overwriting it, so no failure is lost. Entries are not removed on read; call ThrottleStore().SweepExpired(ctx, time.Now()) from the same ticker as SweepExpired.
Upgrading
Migrations0006 to 0008 add session columns and tables (last_seen_at, elevated_until, credential_id, session links), API tokens, device codes, the TOTP step table and the throttle table. Existing sessions read back with a zero LastSeenAt. Run Migrate (or fold the new files into your migrator) before deploying.
Behavior notes
- Email uniqueness is case-insensitive (
COLLATE NOCASE, ASCII folding). A duplicate returns the driver’s constraint error wrapped with context. - IDs are stored as 26 character ULID text, timestamps as UTC unix microseconds.
- Consuming a magic link, reset token or session link, and claiming a device code, is one
UPDATE ... RETURNING, so concurrent consumers produce exactly one winner.AdvanceTOTPStepis one conditional upsert. - Audit rows carry no foreign keys: the log is append-only and survives deletion of the user it describes.
UpdateWebAuthnSignCountreturnsErrReplayDetectedfor a non-increasing count.