Kysely Migration Generation#
The Kysely migration generator is the engine behind better-auth migrate and the generate CLI command when targeting a Kysely adapter. Its entry point is getMigrations in packages/better-auth/src/db/get-migration.ts. The CLI-facing generateKyselySchema wrapper in packages/cli/src/generators/kysely.ts simply calls getMigrations(options) and then compileMigrations() to emit raw SQL into a timestamped .sql file.
How It Works#
-
Schema discovery.
getMigrationscallsgetSchema(config)to obtain the fullBetterAuthDBSchemaderived from all active plugins . -
Table diffing. The function introspects the live database via
db.introspection.getTables()and compares it against the schema, splitting tables into two buckets :toBeCreated— tables not yet present in the databasetoBeAdded— existing tables that are missing columns
-
CREATE TABLEgeneration. For eachtoBeCreatedentry, the generator calls Kysely'sschema.createTable(). Every field is added withaddColumn, attachingNOT NULL, foreign-key references, and — iffield.uniqueis set — an inline.unique()constraint on the column . -
ALTER TABLEgeneration. For eachtoBeAddedentry, the generator usesschema.alterTable().addColumn()with the same column-level constraints . -
Deferred indexes. Both code paths collect
CREATE INDEX/CREATE UNIQUE INDEXbuilders in adeferredIndexesarray and append them after allCREATE TABLE/ALTER TABLEstatements. This ensures referenced tables and columns exist before any index is created . -
SQL compilation.
compileMigrations()calls.compile().sqlon every builder and joins them with;\n\n.
Type Mapping#
getType maps DBFieldAttribute.type to a database-specific Kysely ColumnDataType. Key rules:
- MySQL
stringfields widen tovarchar(255)whenunique,sortable, orindexis set (otherwisetext) . datefields map totimestamptz(postgres),timestamp(3)(mysql),date(sqlite), ordatetime2(3)(mssql) .idandforeignKeyIdfields honour theadvanced.database.generateIdoption to emituuid,integer GENERATED BY DEFAULT AS IDENTITY, orvarchar(36).
The Redundant Uniqueness Bug#
A field with both unique: true and index: true triggers two independent code paths that don't coordinate:
-
Column-level
UNIQUEconstraint — theif (field.unique)guard at line 526 (CREATE TABLE path) and line 461 (ALTER TABLE path) unconditionally appends.unique()to the column builder. -
Table-level unique index — the
if (field.index)guard at lines 543–551 (CREATE TABLE) and lines 438–447 (ALTER TABLE) pushes aCREATE UNIQUE INDEX … ON table (column)intodeferredIndexeswheneverfield.uniqueis also true.
The result is that the compiled SQL contains both a UNIQUE column constraint and a separate CREATE UNIQUE INDEX for the same column — effectively two uniqueness enforcement mechanisms for one field. A concrete real-world trigger is the organization.slug field from the organization plugin, which carries { unique: true, index: true } in the built-in schema .
The analogous issue was confirmed and fixed for the Drizzle schema generator in PR #10333; the fix suppresses the column-level .unique() call when a table-level uniqueIndex will already be emitted. As of writing, the Kysely get-migration.ts file has not received the same guard.
Key Files#
| File | Purpose |
|---|---|
packages/better-auth/src/db/get-migration.ts | Core migration builder: schema diff, CREATE TABLE, ALTER TABLE, deferred indexes |
packages/cli/src/generators/kysely.ts | CLI entry point; calls compileMigrations() and writes the .sql output file |