fix(api): reconcile better-auth schema with installed better-auth CLI output

The Task 3 schema drifted from what better-auth@1.6.18's CLI generates: it used
mode:'timestamp' (epoch seconds) instead of 'timestamp_ms', left verification
timestamps nullable, and omitted the default expressions and helper indexes.

Regenerated src/db/schema.ts from `@better-auth/cli generate` (authoritative per
the plan's version-drift rule) and rebuilt migration 0000 from scratch (no data
exists yet). Converted the index callbacks from the CLI's array form to the
object form required by drizzle-orm@0.36.4's types. Adds session_userId_idx,
account_userId_idx, verification_identifier_idx and the unixepoch defaults.

Tests (health, db, auth sign-up/sign-in, /api/me round-trip) all pass; typecheck
clean; db:generate reports no pending changes.
This commit is contained in:
Bas van Rossem
2026-06-17 14:06:16 +02:00
parent bcb305b048
commit 41b65f209c
4 changed files with 153 additions and 60 deletions

View File

@@ -10,16 +10,17 @@ CREATE TABLE `account` (
`refresh_token_expires_at` integer, `refresh_token_expires_at` integer,
`scope` text, `scope` text,
`password` text, `password` text,
`created_at` integer NOT NULL, `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL, `updated_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
); );
--> statement-breakpoint --> statement-breakpoint
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
CREATE TABLE `session` ( CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL, `id` text PRIMARY KEY NOT NULL,
`expires_at` integer NOT NULL, `expires_at` integer NOT NULL,
`token` text NOT NULL, `token` text NOT NULL,
`created_at` integer NOT NULL, `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL, `updated_at` integer NOT NULL,
`ip_address` text, `ip_address` text,
`user_agent` text, `user_agent` text,
@@ -28,14 +29,15 @@ CREATE TABLE `session` (
); );
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
CREATE TABLE `user` ( CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL, `id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`email` text NOT NULL, `email` text NOT NULL,
`email_verified` integer DEFAULT false NOT NULL, `email_verified` integer DEFAULT false NOT NULL,
`image` text, `image` text,
`created_at` integer NOT NULL, `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
@@ -44,6 +46,8 @@ CREATE TABLE `verification` (
`identifier` text NOT NULL, `identifier` text NOT NULL,
`value` text NOT NULL, `value` text NOT NULL,
`expires_at` integer NOT NULL, `expires_at` integer NOT NULL,
`created_at` integer, `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
); );
--> statement-breakpoint
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);

View File

@@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "4b21759d-56d7-435a-a345-205a6cdc017e", "id": "e8545186-09fa-4515-bca1-891ed8364a07",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"account": { "account": {
@@ -89,7 +89,8 @@
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}, },
"updated_at": { "updated_at": {
"name": "updated_at", "name": "updated_at",
@@ -99,7 +100,15 @@
"autoincrement": false "autoincrement": false
} }
}, },
"indexes": {}, "indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": [
"user_id"
],
"isUnique": false
}
},
"foreignKeys": { "foreignKeys": {
"account_user_id_user_id_fk": { "account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
@@ -148,7 +157,8 @@
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}, },
"updated_at": { "updated_at": {
"name": "updated_at", "name": "updated_at",
@@ -186,6 +196,13 @@
"token" "token"
], ],
"isUnique": true "isUnique": true
},
"session_userId_idx": {
"name": "session_userId_idx",
"columns": [
"user_id"
],
"isUnique": false
} }
}, },
"foreignKeys": { "foreignKeys": {
@@ -251,14 +268,16 @@
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}, },
"updated_at": { "updated_at": {
"name": "updated_at", "name": "updated_at",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
} }
}, },
"indexes": { "indexes": {
@@ -310,18 +329,28 @@
"name": "created_at", "name": "created_at",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": true,
"autoincrement": false "autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}, },
"updated_at": { "updated_at": {
"name": "updated_at", "name": "updated_at",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": true,
"autoincrement": false "autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": [
"identifier"
],
"isUnique": false
} }
}, },
"indexes": {},
"foreignKeys": {}, "foreignKeys": {},
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {}, "uniqueConstraints": {},

View File

@@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1781696310538, "when": 1781697895207,
"tag": "0000_youthful_genesis", "tag": "0000_stiff_captain_britain",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -1,29 +1,51 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; import { relations, sql } from 'drizzle-orm';
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
// better-auth core tables. This schema is generated by the better-auth CLI
// (`npx @better-auth/cli generate --config src/auth.ts`) and is the AUTHORITATIVE
// shape for the installed better-auth version — do not hand-edit; regenerate instead.
export const user = sqliteTable('user', { export const user = sqliteTable('user', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
name: text('name').notNull(), name: text('name').notNull(),
email: text('email').notNull().unique(), email: text('email').notNull().unique(),
emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false), emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
image: text('image'), image: text('image'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp_ms' })
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => new Date())
.notNull(),
}); });
export const session = sqliteTable('session', { export const session = sqliteTable(
'session',
{
id: text('id').primaryKey(), id: text('id').primaryKey(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
token: text('token').notNull().unique(), token: text('token').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp_ms' })
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.$onUpdate(() => new Date())
.notNull(),
ipAddress: text('ip_address'), ipAddress: text('ip_address'),
userAgent: text('user_agent'), userAgent: text('user_agent'),
userId: text('user_id') userId: text('user_id')
.notNull() .notNull()
.references(() => user.id, { onDelete: 'cascade' }), .references(() => user.id, { onDelete: 'cascade' }),
}); },
(table) => ({
sessionUserIdIdx: index('session_userId_idx').on(table.userId),
})
);
export const account = sqliteTable('account', { export const account = sqliteTable(
'account',
{
id: text('id').primaryKey(), id: text('id').primaryKey(),
accountId: text('account_id').notNull(), accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(), providerId: text('provider_id').notNull(),
@@ -33,19 +55,57 @@ export const account = sqliteTable('account', {
accessToken: text('access_token'), accessToken: text('access_token'),
refreshToken: text('refresh_token'), refreshToken: text('refresh_token'),
idToken: text('id_token'), idToken: text('id_token'),
accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp' }), accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp_ms' }),
refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }), refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp_ms' }),
scope: text('scope'), scope: text('scope'),
password: text('password'), password: text('password'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp_ms' })
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
}); .notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.$onUpdate(() => new Date())
.notNull(),
},
(table) => ({
accountUserIdIdx: index('account_userId_idx').on(table.userId),
})
);
export const verification = sqliteTable('verification', { export const verification = sqliteTable(
'verification',
{
id: text('id').primaryKey(), id: text('id').primaryKey(),
identifier: text('identifier').notNull(), identifier: text('identifier').notNull(),
value: text('value').notNull(), value: text('value').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }), createdAt: integer('created_at', { mode: 'timestamp_ms' })
updatedAt: integer('updated_at', { mode: 'timestamp' }), .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
}); .notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => new Date())
.notNull(),
},
(table) => ({
verificationIdentifierIdx: index('verification_identifier_idx').on(table.identifier),
})
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));