Better Auth
sqlfu/better-auth lets Better Auth own its schema model while sqlfu remains
the migration owner for the project.
The intended loop is:
- Better Auth generates the auth-owned tables into a managed section of
definitions.sql. sqlfu draftcompares the updated desired schema with migration history.sqlfu migrateapplies the reviewed migration to the database.
That keeps the Better Auth config as the source for auth tables without making Better Auth write sqlfu migration files or diff your live database.
Install and configure
Section titled “Install and configure”Use the adapter in the Better Auth config that you pass to auth generate:
import {betterAuth} from 'better-auth';import {sqlfuBetterAuthAdapter} from 'sqlfu/better-auth';
export const auth = betterAuth({ database: sqlfuBetterAuthAdapter(),});sqlfuBetterAuthAdapter() resolves sqlfu.config.* from the current working
directory. The configured definitions file is the only file it will write.
A matching sqlfu config can stay ordinary:
import {defineConfig} from 'sqlfu';
export default defineConfig({ definitions: './definitions.sql', migrations: {path: './migrations'}, queries: './sql',});Then run:
npx auth@latest generate --yesnpx sqlfu draftnpx sqlfu migrateIf you pass --output, it must resolve to the same file as
sqlfuConfig.definitions. If you omit --output, sqlfu uses the configured
definitions file.
What gets written
Section titled “What gets written”The adapter writes one managed region:
-- #region sqlfu:better-auth-- generated by Better Auth through sqlfuBetterAuthAdapter; edit Better Auth config insteadcreate table "user" ( "id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null);-- #endregion sqlfu:better-authGenerated SQL is formatted with sqlfu’s formatter. SQL outside the managed region is preserved byte-for-byte.
On the first run:
- an empty or missing
definitions.sqlbecomes just the managed Better Auth region; - a nonempty file with no managed region gets the region appended if the combined application schema plus Better Auth schema applies cleanly to a scratch SQLite database;
- a nonempty file that already conflicts with Better Auth, for example an
app-owned
"user"table, fails instead of guessing which schema should win.
After the first run, auth generate replaces the existing managed region. That
means Better Auth plugin changes, extra user fields, and removed auth tables all
flow back through the same block.
Existing application schema
Section titled “Existing application schema”You do not need to add the markers by hand for a normal first run. This file:
create table someotherthing(id int, name text);can become:
create table someotherthing(id int, name text);
-- #region sqlfu:better-auth-- generated by Better Auth through sqlfuBetterAuthAdapter; edit Better Auth config insteadcreate table "user" ( "id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null);
create table "session" ( "id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id") on delete cascade);-- #endregion sqlfu:better-authThe scratch database check is intentionally narrow. It proves the desired schema
is internally applyable from empty SQLite. It does not prove that the change is
safe for your live database; that is still what sqlfu draft, review, and
sqlfu migrate are for.
Runtime auth adapters
Section titled “Runtime auth adapters”Calling sqlfuBetterAuthAdapter() with no arguments is for schema generation.
Runtime create/read/update/delete methods throw with a message telling you to
pass an underlying Better Auth adapter.
If you want one Better Auth config to handle both generation and runtime, pass
an adapter factory and sqlfu will delegate runtime methods to it while still
overriding createSchema:
import {betterAuth} from 'better-auth';import {kyselyAdapter} from 'better-auth/adapters/kysely';import {sqlfuBetterAuthAdapter} from 'sqlfu/better-auth';
export const auth = betterAuth({ database: sqlfuBetterAuthAdapter({ adapter: kyselyAdapter(db, {type: 'sqlite'}), }),});This wrapper is tested for the schema-generation path. Validate runtime auth behavior through the underlying Better Auth adapter in your application. If production already uses Better Auth’s direct D1 support or another runtime path, keep that config and use a small CLI-only auth config for generation.
Troubleshooting
Section titled “Troubleshooting”auth generate says the output file is wrong. The adapter only writes
sqlfuConfig.definitions. Run without --output, or pass the configured
definitions path.
First-time append fails. The combined desired schema did not apply to a
scratch SQLite database. Common causes are an app-owned auth table name, a
syntax error in the existing definitions.sql, or application DDL that relies
on objects that are not present in the file.
Wrapped adapter naming options do not affect generated table names. The
schema SQL comes from Better Auth’s Kysely migration compiler against an empty
SQLite database. Options such as usePlural on a wrapped adapter are not part
of the supported schema-generation contract.
auth generate returns no changes. If the managed Better Auth region is
already up to date, the adapter returns an empty code body to the Better Auth
CLI.