effect-auth

Storage

Public storage service contracts, result semantics, atomic operations, SQLite adapters, and migrations.

Effect Auth exposes focused Effect services, not one Storage service. Programs require only tags used by enabled features. Exported *StoreService, record, row, and input types are the authoritative signatures; construct implementations with each tag's make function.

For deployment choices and ownership, see Storage Overview. For implementation and concurrency guidance, see Custom Database.

Base services

The base contracts are exported from @effect-auth/core/Storage and fail with StorageError.

ServiceRows and responsibilityResult-sensitive mutations
UserStoreIdentity-neutral NewUserRow / UserRow; lookup by ID and insertUser lifecycle stays separate from identifiers and credentials
IdentityStoreIdentity lookup and lifecycle for global email and global/tenant username or custom kindsVerification, primary selection, replacement, and revocation are identity-scoped CAS mutations; active-value uniqueness permits immediate reuse after revoke/replace
RegistrationStoreAtomic user + identity + password credential creationThe transaction either commits all three records or none
CredentialStoreNewPasswordCredentialRow / PasswordCredentialRow; password lookup, insert, updateupdatePassword returns the updated row as Option
SessionStoreNewSessionRow / SessionRow; server-only authentication events, derived summaries, lookup, refresh, rotation, claims, revocationassureAndRotate compare-and-swaps expected evidence/secret state, writes derived summaries, rotates the secret, and invalidates the old token atomically; omitted claim fields are preserved
VerificationStoreNewVerificationRow / VerificationRow; one-time challenges and cleanupconsume returns Option and is atomic; deleteExpired returns the deleted count

SessionStore.findById returns stored rows even when expired or revoked; policy interprets lifecycle fields. VerificationStore.consume succeeds only when a row exists, is unconsumed, matches an optional type, and has expiresAt > consumedAt. Concurrent consumers have at most one winner.

StorageError carries entity, operation, message, and optional cause. StorageError.fromUnknown(entity, operation, cause) maps backend failures without exposing row contents or secrets.

Feature-owned services

Feature contracts and errors live in the named public module. The maintained SQLite bundle implements these services. * requires eligibility and mutation in one atomic statement or transaction; CAS also requires the expected prior state.

ModulePublic serviceContract summary and atomic operations
AuditLogAuditLogStoreappend/query audit events
LoginApprovalLoginApprovalReviewStorecreate and review approval records; review transition *
LoginRiskLoginRiskHistoryrecord/query risk history
TrustedDeviceTrustedDeviceStoredevice lookup, touch, revoke, cleanup
PasskeyPasskeyCredentialStorecredential lookup/list; sign-count update CAS; revoke
TotpTotpFactorStorefactor lifecycle; confirm and last-used update *
RecoveryCodeRecoveryCodeStoreissue/list/revoke codes; one-time use *
ApiKeyApiKeyStoreID/prefix lookup, list, use update, revoke
RefreshTokenRefreshTokenStoreID/hash lookup and family lifecycle; rotation inserts the replacement and retires the source atomically *; reuse marking
JwtJwtRevocationStorestore and find JWT revocations; failures use JwtError
OAuthOAuthAccountStore, OAuthIdentityBridgeStoreprovider-account links and unlinking; atomic user + optional email identity + provider-account creation for OAuth JIT
OAuthOAuthClientStore, OAuthConsentStore, OAuthClientSecretStoreclients, consent, and hashed client secrets
OAuthOAuthAuthorizationCodeStoreauthorization-code lookup and one-time markConsumed *
OAuthOAuthProviderModeTokenStore, OAuthProviderTokenVaultprovider-token and encrypted-vault lifecycle; rotation *
DomainVerificationDomainVerificationStoreowner/domain lookup and status transitions *
SecurityTimelineSecurityTimelineStoreappend and query security events
WebhookWebhookOutboxStoreenqueue, lease due work *, mark outcomes, delete old rows
WebhookWebhookReplayStoreidempotent replay claim * and expiry cleanup
PermissionPermissionStoreCAS-protected definition lifecycle; direct and role grants; role mappings; deterministic active/inactive/exact-scope queries; exact/global authorization checks

RateLimitStore is public but is not in the maintained SQLite bundle. Supply a feature-specific implementation when required.

IdentityManagement relies on storage, not a preflight availability query, for correctness. Replace, revoke, and primary mutations match owner, active state, and expectedUpdatedAt; revoke additionally prevents removing the final login-eligible identity. Custom adapters must make those checks and writes atomic. The OAuth bridge likewise commits its new user, optional trusted verified-email identity, and provider account together. A provider-account race resolves to the winner; an email uniqueness race becomes an explicit-linking result rather than a partial user or silent merge.

Results, time, and encoding

ShapeMeaning
Option.some(row)Lookup found a row, or a conditional mutation won and returns its resulting row
Option.none()Missing or ineligible row; successful operation, not storage failure
readonly Row[]Successful list; no matches is an empty array
booleanClaim/admission result, as defined by the owning service
numberAffected-row count for cleanup or deletion
voidSuccess; missing rows may be an intentional idempotent no-op

Driver, timeout, uniqueness, serialization, and decode failures belong in the Effect error channel: StorageError for base stores and the owning module's error elsewhere. Never convert backend failures to Option.none().

Identifiers are branded runtime strings. UnixMillis values are finite epoch-millisecond numbers; use exact boundaries and caller-supplied mutation times, not database time. Preserve optional fields. Metadata, claims, scopes, canonical AMR, transports, request context, and versioned authentication events must round-trip losslessly; SQLite encodes structures as JSON text and booleans as compatible values. Materialized AAL/AMR/timestamps must match central derivation on every read and write. Raw events are security provenance: keep them server-side and never include them in ordinary session HTTP responses. Store hashes and ciphertext unchanged.

Maintained SQLite entry points

Public moduleEntry point
EffectQbSqliteStoragemakeEffectQbSqliteStorage(executor, options?), EffectQbSqliteAuthStorageLive(executor, options?), D1EffectQbSqliteAuthStorageLive(...)
DrizzleEffectSqliteStoragemakeDrizzleEffectSqliteStorage(database, options?), DrizzleEffectSqliteAuthStorageLive(database, options?)
DrizzleD1SqliteStoragemakeD1DrizzleEffectSqliteStorage(...), DrizzleD1SqliteAuthStorageLive(...)
DrizzleNodeSqliteStorage / DrizzleBunSqliteStoragemakeNodeDrizzleEffectSqliteStorage(...) / makeBunDrizzleEffectSqliteStorage(...) and matching *AuthStorageLive layers
DrizzleDurableObjectSqliteStoragemakeDurableObjectDrizzleEffectSqliteStorage(...), DrizzleDurableObjectSqliteAuthStorageLive(...)
DrizzleWasmSqliteStoragepersistent and memory makeWasm*Storage(...) functions and matching DrizzleWasm*AuthStorageLive layers

All return the same service set; options can replace AuthSqliteTables and tune login-risk history. Constructors acquiring a client are scoped. Provided executors and databases remain application-owned.

Migrations

authStorageMigrations from @effect-auth/core/StorageMigrations is the authoritative ordered array of AuthStorageMigration { id, sql }. It contains 0001_auth_storage through 0026_auth_user_identity, covering the maintained bundle. Apply each entry once, in order, before traffic and record its id. Migration 0026_auth_user_identity upgrades the published email-first user schema to identity-first storage and backfills each existing email as the user's primary global identity. Adapters never migrate automatically.

Assurance v2 alpha reset

Assurance v2 rewrote the baseline 0001_auth_storage and 0006_auth_totp_factor migrations to add session evidence and the TOTP replay floor. There is no compatibility read, backfill, or upgrade migration. Reset development databases created by an earlier alpha and apply the current baseline from empty storage. Do not apply the rewritten SQL over a database that already recorded those migration IDs.

On this page