Skip to content
pre-alpha — the TypeScript API may still shift. The SQL won't.

sqlfu/better-auth lets Better Auth own its schema model while sqlfu remains the migration owner for the project.

The intended loop is:

  1. Better Auth generates the auth-owned tables into a managed section of definitions.sql.
  2. sqlfu draft compares the updated desired schema with migration history.
  3. sqlfu migrate applies 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.

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:

Terminal window
npx auth@latest generate --yes
npx sqlfu draft
npx sqlfu migrate

If you pass --output, it must resolve to the same file as sqlfuConfig.definitions. If you omit --output, sqlfu uses the configured definitions file.

The adapter writes one managed region:

-- #region sqlfu:better-auth
-- generated by Better Auth through sqlfuBetterAuthAdapter; edit Better Auth config instead
create 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-auth

Generated 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.sql becomes 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.

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 instead
create 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-auth

The 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.

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.

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.