Using custom column names causes database failures #1439

Closed
opened 2026-03-13 08:40:16 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @rebasecase on GitHub (Jun 28, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Create tables and config as below
Try and register/sign in

import type { Kysely } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
  /*
    Table Name: user

    Field Name	Type	Key	Description
    id	string	PK	Unique identifier for each user
    name	string	-	User's chosen display name
    email	string	-	User's email address for communication and login
    emailVerified	boolean	-	Whether the user's email is verified
    image	string	?	User's image url
    createdAt	Date	-	Timestamp of when the user account was created
    updatedAt	Date	-	Timestamp of the last update to the user's information
  */
  await db.schema
    .createTable('user')
    .addColumn('id', 'uuid', (col) => col.primaryKey())
    .addColumn('name', 'varchar', (col) => col.notNull())
    .addColumn('email', 'varchar', (col) => col.notNull().unique())
    .addColumn('email_verified', 'boolean', (col) => col.notNull().defaultTo(false))
    .addColumn('image', 'varchar')
    .addColumn('created_at', 'timestamp', (col) => col.notNull())
    .addColumn('updated_at', 'timestamp', (col) => col.notNull())
    .execute();

  /*
    Table Name: session

    Field Name	Type	Key	Description
    id	string	PK	Unique identifier for each session
    userId	string	FK	The ID of the user
    token	string	-	The unique session token
    expiresAt	Date	-	The time when the session expires
    ipAddress	string	?	The IP address of the device
    userAgent	string	?	The user agent information of the device
    createdAt	Date	-	Timestamp of when the session was created
    updatedAt	Date	-	Timestamp of when the session was updated
  */
  await db.schema
    .createTable('session')
    .addColumn('id', 'uuid', (col) => col.primaryKey())
    .addColumn('user_id', 'uuid', (col) => col.references('user.id').notNull().onDelete('cascade'))
    .addColumn('token', 'varchar', (col) => col.notNull().unique())
    .addColumn('expires_at', 'timestamp', (col) => col.notNull())
    .addColumn('ip_address', 'varchar')
    .addColumn('user_agent', 'varchar')
    .addColumn('created_at', 'timestamp', (col) => col.notNull())
    .addColumn('updated_at', 'timestamp', (col) => col.notNull())
    .execute();

  /*
    Table Name: account

    Field Name	Type	Key	Description
    id	string	PK	Unique identifier for each account
    userId	string	FK	The ID of the user
    accountId	string	-	The ID of the account as provided by the SSO or equal to userId for credential accounts
    providerId	string	-	The ID of the provider
    accessToken	string	?	The access token of the account. Returned by the provider
    refreshToken	string	?	The refresh token of the account. Returned by the provider
    accessTokenExpiresAt	Date	?	The time when the access token expires
    refreshTokenExpiresAt	Date	?	The time when the refresh token expires
    scope	string	?	The scope of the account. Returned by the provider
    idToken	string	?	The ID token returned from the provider
    password	string	?	The password of the account. Mainly used for email and password authentication
    createdAt	Date	-	Timestamp of when the account was created
    updatedAt	Date	-	Timestamp of when the account was updated
  */
  await db.schema
    .createTable('account')
    .addColumn('id', 'uuid', (col) => col.primaryKey())
    .addColumn('user_id', 'uuid', (col) => col.references('user.id').notNull().onDelete('cascade'))
    .addColumn('account_id', 'varchar', (col) => col.notNull())
    .addColumn('provider_id', 'varchar', (col) => col.notNull())
    .addColumn('access_token', 'varchar')
    .addColumn('refresh_token', 'varchar')
    .addColumn('access_token_expires_at', 'timestamp')
    .addColumn('refresh_token_expires_at', 'timestamp')
    .addColumn('scope', 'varchar')
    .addColumn('id_token', 'varchar')
    .addColumn('password', 'varchar')
    .addColumn('created_at', 'timestamp', (col) => col.notNull())
    .addColumn('updated_at', 'timestamp', (col) => col.notNull())
    .execute();

  /*
    Table Name: verification

    Field Name	Type	Key	Description
    id	string	PK	Unique identifier for each verification
    identifier	string	-	The identifier for the verification request
    value	string	-	The value to be verified
    expiresAt	Date	-	The time when the verification request expires
    createdAt	Date	-	Timestamp of when the verification request was created
    updatedAt	Date	-	Timestamp of when the verification request was updated
  */
  await db.schema
    .createTable('verification')
    .addColumn('id', 'uuid', (col) => col.primaryKey())
    .addColumn('identifier', 'varchar', (col) => col.notNull())
    .addColumn('value', 'varchar', (col) => col.notNull())
    .addColumn('expires_at', 'timestamp', (col) => col.notNull())
    .addColumn('created_at', 'timestamp', (col) => col.notNull())
    .addColumn('updated_at', 'timestamp', (col) => col.notNull())
    .execute();
}

// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
export async function down(db: Kysely<any>): Promise<void> {
  // down migration code goes here...
  // note: down migrations are optional. you can safely delete this function.
  // For more info, see: https://kysely.dev/docs/migrations
  await db.schema.dropTable('verification').ifExists().execute();
  await db.schema.dropTable('account').ifExists().execute();
  await db.schema.dropTable('session').ifExists().execute();
  await db.schema.dropTable('user').ifExists().execute();
}

Current vs. Expected behavior

w.g. logging in with asd@asd.com:asd@asd.com

  VITE v7.0.0  ready in 1718 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
2025-06-28T20:01:01.450Z INFO [Better Auth]: [Kysely Adapter] #0 [1/3] findOne: {
  model: 'user',
  where: [
    {
      operator: 'eq',
      connector: 'AND',
      field: 'email',
      value: 'asd@asd.com'
    }
  ],
  select: undefined
}
2025-06-28T20:01:01.461Z INFO [Better Auth]: [Kysely Adapter] #0 [2/3] findOne (DB Result): {
  model: 'user',
  data: {
    id: '0197b80f-6b2b-7daf-9c9f-2da4662354a7',
    name: 'name2',
    email: 'asd@asd.com',
    emailVerified: false,
    image: null,
    createdAt: 2025-06-28T19:41:47.691Z,
    updatedAt: 2025-06-28T19:41:47.691Z
  }
}
2025-06-28T20:01:01.462Z INFO [Better Auth]: [Kysely Adapter] #0 [3/3] findOne (Parsed Result): {
  model: 'user',
  data: {
    name: 'name2',
    email: 'asd@asd.com',
    emailVerified: undefined,
    image: null,
    createdAt: undefined,
    updatedAt: undefined,
    id: '0197b80f-6b2b-7daf-9c9f-2da4662354a7'
  }
}
2025-06-28T20:01:01.462Z INFO [Better Auth]: [Kysely Adapter] #1 [1/3] findMany: {
  model: 'account',
  where: [
    {
      operator: 'eq',
      connector: 'AND',
      field: 'user_id',
      value: '0197b80f-6b2b-7daf-9c9f-2da4662354a7'
    }
  ],
  limit: 100,
  sortBy: undefined,
  offset: undefined
}
2025-06-28T20:01:01.463Z INFO [Better Auth]: [Kysely Adapter] #1 [2/3] findMany (DB Result): {
  model: 'account',
  data: [
    {
      id: '0197b80f-6b2f-7a34-bbc8-38cdfbbe8295',
      userId: '0197b80f-6b2b-7daf-9c9f-2da4662354a7',
      accountId: '0197b80f-6b2b-7daf-9c9f-2da4662354a7',
      providerId: 'credential',
      accessToken: null,
      refreshToken: null,
      accessTokenExpiresAt: null,
      refreshTokenExpiresAt: null,
      scope: null,
      idToken: null,
      password: 'daed858f7f14cf3b0b3c177b0c06a59a:e91593c148b6559f9e4b0ded563001d9507c0801fbc6bf8d5ad3074b406c70eee04a78a558e077d5e44b603f723a9ecbe3c54260b647173417ca9a17d4a0e282',
      createdAt: 2025-06-28T19:41:47.695Z,
      updatedAt: 2025-06-28T19:41:47.695Z
    }
  ]
}
2025-06-28T20:01:01.464Z INFO [Better Auth]: [Kysely Adapter] #1 [3/3] findMany (Parsed Result): {
  model: 'account',
  data: [
    {
      accountId: undefined,
      providerId: undefined,
      userId: undefined,
      accessToken: undefined,
      refreshToken: undefined,
      idToken: undefined,
      accessTokenExpiresAt: undefined,
      refreshTokenExpiresAt: undefined,
      scope: null,
      password: 'daed858f7f14cf3b0b3c177b0c06a59a:e91593c148b6559f9e4b0ded563001d9507c0801fbc6bf8d5ad3074b406c70eee04a78a558e077d5e44b603f723a9ecbe3c54260b647173417ca9a17d4a0e282',
      createdAt: undefined,
      updatedAt: undefined,
      id: '0197b80f-6b2f-7a34-bbc8-38cdfbbe8295'
    }
  ]
}
2025-06-28T20:01:01.464Z ERROR [Better Auth]: Credential account not found { email: 'asd@asd.com' }

What version of Better Auth are you using?

1.2.12

Provide environment information

-

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

export const auth = betterAuth({
  database: {
    db,
    type: 'postgres'
    //debugLogs: true
  },
  user: {
    fields: {
      emailVerified: 'email_verified',
      createdAt: 'created_at',
      updatedAt: 'updated_at'
    }
  },
  session: {
    fields: {
      userId: 'user_id',
      expiresAt: 'expires_at',
      createdAt: 'created_at',
      updatedAt: 'updated_at',
      ipAddress: 'ip_address',
      userAgent: 'user_agent'
    }
  },
  account: {
    fields: {
      userId: 'user_id',
      accountId: 'account_id',
      providerId: 'provider_id',
      accessToken: 'access_token',
      refreshToken: 'refresh_token',
      idToken: 'id_token',
      accessTokenExpiresAt: 'access_token_expires_at',
      refreshTokenExpiresAt: 'refresh_token_expires_at',
      createdAt: 'created_at',
      updatedAt: 'updated_at'
    }
  },
  verification: {
    fields: {
      expiresAt: 'expires_at',
      createdAt: 'created_at',
      updatedAt: 'updated_at'
    }
  },
  advanced: {
    database: {
      generateId: uuidv7
    }
  },
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false,
    autoSignIn: false
  }
});

Additional context

Without the additional config it works fine.

My DB config is as such

export const db = new Kysely<Database>({
  dialect: new PostgresDialect({
    pool: new Pool({
      connectionString: env.DATABASE_URL,
      max: 10
    })
  }),
  plugins: [new CamelCasePlugin({ underscoreBeforeDigits: true })]
});

Migrations say this, so that is at least working:

⠋ preparing migration...2025-06-28T20:03:16.359Z WARN [Better Auth]: Field name in table user has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field email in table user has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field image in table user has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field token in table session has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field ip_address in table session has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field user_agent in table session has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field user_id in table session has a different type in the database. Expected string but got uuid.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field account_id in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field provider_id in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field user_id in table account has a different type in the database. Expected string but got uuid.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field access_token in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field refresh_token in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field id_token in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field scope in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field password in table account has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field identifier in table verification has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.359Z WARN [Better Auth]: Field value in table verification has a different type in the database. Expected string but got varchar.
2025-06-28T20:03:16.360Z INFO [Better Auth]: 🚀 No migrations needed.

I suspect it's to do with my snakecase'd columns, but without the custom table mappings it works correctly. The Kysely plugins approach seems clearer to me. More transparent in terms of magic, less transparent in terms of explicitness

Originally created by @rebasecase on GitHub (Jun 28, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Create tables and config as below Try and register/sign in ```ts import type { Kysely } from 'kysely'; export async function up(db: Kysely<any>): Promise<void> { /* Table Name: user Field Name Type Key Description id string PK Unique identifier for each user name string - User's chosen display name email string - User's email address for communication and login emailVerified boolean - Whether the user's email is verified image string ? User's image url createdAt Date - Timestamp of when the user account was created updatedAt Date - Timestamp of the last update to the user's information */ await db.schema .createTable('user') .addColumn('id', 'uuid', (col) => col.primaryKey()) .addColumn('name', 'varchar', (col) => col.notNull()) .addColumn('email', 'varchar', (col) => col.notNull().unique()) .addColumn('email_verified', 'boolean', (col) => col.notNull().defaultTo(false)) .addColumn('image', 'varchar') .addColumn('created_at', 'timestamp', (col) => col.notNull()) .addColumn('updated_at', 'timestamp', (col) => col.notNull()) .execute(); /* Table Name: session Field Name Type Key Description id string PK Unique identifier for each session userId string FK The ID of the user token string - The unique session token expiresAt Date - The time when the session expires ipAddress string ? The IP address of the device userAgent string ? The user agent information of the device createdAt Date - Timestamp of when the session was created updatedAt Date - Timestamp of when the session was updated */ await db.schema .createTable('session') .addColumn('id', 'uuid', (col) => col.primaryKey()) .addColumn('user_id', 'uuid', (col) => col.references('user.id').notNull().onDelete('cascade')) .addColumn('token', 'varchar', (col) => col.notNull().unique()) .addColumn('expires_at', 'timestamp', (col) => col.notNull()) .addColumn('ip_address', 'varchar') .addColumn('user_agent', 'varchar') .addColumn('created_at', 'timestamp', (col) => col.notNull()) .addColumn('updated_at', 'timestamp', (col) => col.notNull()) .execute(); /* Table Name: account Field Name Type Key Description id string PK Unique identifier for each account userId string FK The ID of the user accountId string - The ID of the account as provided by the SSO or equal to userId for credential accounts providerId string - The ID of the provider accessToken string ? The access token of the account. Returned by the provider refreshToken string ? The refresh token of the account. Returned by the provider accessTokenExpiresAt Date ? The time when the access token expires refreshTokenExpiresAt Date ? The time when the refresh token expires scope string ? The scope of the account. Returned by the provider idToken string ? The ID token returned from the provider password string ? The password of the account. Mainly used for email and password authentication createdAt Date - Timestamp of when the account was created updatedAt Date - Timestamp of when the account was updated */ await db.schema .createTable('account') .addColumn('id', 'uuid', (col) => col.primaryKey()) .addColumn('user_id', 'uuid', (col) => col.references('user.id').notNull().onDelete('cascade')) .addColumn('account_id', 'varchar', (col) => col.notNull()) .addColumn('provider_id', 'varchar', (col) => col.notNull()) .addColumn('access_token', 'varchar') .addColumn('refresh_token', 'varchar') .addColumn('access_token_expires_at', 'timestamp') .addColumn('refresh_token_expires_at', 'timestamp') .addColumn('scope', 'varchar') .addColumn('id_token', 'varchar') .addColumn('password', 'varchar') .addColumn('created_at', 'timestamp', (col) => col.notNull()) .addColumn('updated_at', 'timestamp', (col) => col.notNull()) .execute(); /* Table Name: verification Field Name Type Key Description id string PK Unique identifier for each verification identifier string - The identifier for the verification request value string - The value to be verified expiresAt Date - The time when the verification request expires createdAt Date - Timestamp of when the verification request was created updatedAt Date - Timestamp of when the verification request was updated */ await db.schema .createTable('verification') .addColumn('id', 'uuid', (col) => col.primaryKey()) .addColumn('identifier', 'varchar', (col) => col.notNull()) .addColumn('value', 'varchar', (col) => col.notNull()) .addColumn('expires_at', 'timestamp', (col) => col.notNull()) .addColumn('created_at', 'timestamp', (col) => col.notNull()) .addColumn('updated_at', 'timestamp', (col) => col.notNull()) .execute(); } // `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface. export async function down(db: Kysely<any>): Promise<void> { // down migration code goes here... // note: down migrations are optional. you can safely delete this function. // For more info, see: https://kysely.dev/docs/migrations await db.schema.dropTable('verification').ifExists().execute(); await db.schema.dropTable('account').ifExists().execute(); await db.schema.dropTable('session').ifExists().execute(); await db.schema.dropTable('user').ifExists().execute(); } ``` ### Current vs. Expected behavior w.g. logging in with `asd@asd.com:asd@asd.com` ``` VITE v7.0.0 ready in 1718 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help 2025-06-28T20:01:01.450Z INFO [Better Auth]: [Kysely Adapter] #0 [1/3] findOne: { model: 'user', where: [ { operator: 'eq', connector: 'AND', field: 'email', value: 'asd@asd.com' } ], select: undefined } 2025-06-28T20:01:01.461Z INFO [Better Auth]: [Kysely Adapter] #0 [2/3] findOne (DB Result): { model: 'user', data: { id: '0197b80f-6b2b-7daf-9c9f-2da4662354a7', name: 'name2', email: 'asd@asd.com', emailVerified: false, image: null, createdAt: 2025-06-28T19:41:47.691Z, updatedAt: 2025-06-28T19:41:47.691Z } } 2025-06-28T20:01:01.462Z INFO [Better Auth]: [Kysely Adapter] #0 [3/3] findOne (Parsed Result): { model: 'user', data: { name: 'name2', email: 'asd@asd.com', emailVerified: undefined, image: null, createdAt: undefined, updatedAt: undefined, id: '0197b80f-6b2b-7daf-9c9f-2da4662354a7' } } 2025-06-28T20:01:01.462Z INFO [Better Auth]: [Kysely Adapter] #1 [1/3] findMany: { model: 'account', where: [ { operator: 'eq', connector: 'AND', field: 'user_id', value: '0197b80f-6b2b-7daf-9c9f-2da4662354a7' } ], limit: 100, sortBy: undefined, offset: undefined } 2025-06-28T20:01:01.463Z INFO [Better Auth]: [Kysely Adapter] #1 [2/3] findMany (DB Result): { model: 'account', data: [ { id: '0197b80f-6b2f-7a34-bbc8-38cdfbbe8295', userId: '0197b80f-6b2b-7daf-9c9f-2da4662354a7', accountId: '0197b80f-6b2b-7daf-9c9f-2da4662354a7', providerId: 'credential', accessToken: null, refreshToken: null, accessTokenExpiresAt: null, refreshTokenExpiresAt: null, scope: null, idToken: null, password: 'daed858f7f14cf3b0b3c177b0c06a59a:e91593c148b6559f9e4b0ded563001d9507c0801fbc6bf8d5ad3074b406c70eee04a78a558e077d5e44b603f723a9ecbe3c54260b647173417ca9a17d4a0e282', createdAt: 2025-06-28T19:41:47.695Z, updatedAt: 2025-06-28T19:41:47.695Z } ] } 2025-06-28T20:01:01.464Z INFO [Better Auth]: [Kysely Adapter] #1 [3/3] findMany (Parsed Result): { model: 'account', data: [ { accountId: undefined, providerId: undefined, userId: undefined, accessToken: undefined, refreshToken: undefined, idToken: undefined, accessTokenExpiresAt: undefined, refreshTokenExpiresAt: undefined, scope: null, password: 'daed858f7f14cf3b0b3c177b0c06a59a:e91593c148b6559f9e4b0ded563001d9507c0801fbc6bf8d5ad3074b406c70eee04a78a558e077d5e44b603f723a9ecbe3c54260b647173417ca9a17d4a0e282', createdAt: undefined, updatedAt: undefined, id: '0197b80f-6b2f-7a34-bbc8-38cdfbbe8295' } ] } 2025-06-28T20:01:01.464Z ERROR [Better Auth]: Credential account not found { email: 'asd@asd.com' } ``` ### What version of Better Auth are you using? 1.2.12 ### Provide environment information ```bash - ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: { db, type: 'postgres' //debugLogs: true }, user: { fields: { emailVerified: 'email_verified', createdAt: 'created_at', updatedAt: 'updated_at' } }, session: { fields: { userId: 'user_id', expiresAt: 'expires_at', createdAt: 'created_at', updatedAt: 'updated_at', ipAddress: 'ip_address', userAgent: 'user_agent' } }, account: { fields: { userId: 'user_id', accountId: 'account_id', providerId: 'provider_id', accessToken: 'access_token', refreshToken: 'refresh_token', idToken: 'id_token', accessTokenExpiresAt: 'access_token_expires_at', refreshTokenExpiresAt: 'refresh_token_expires_at', createdAt: 'created_at', updatedAt: 'updated_at' } }, verification: { fields: { expiresAt: 'expires_at', createdAt: 'created_at', updatedAt: 'updated_at' } }, advanced: { database: { generateId: uuidv7 } }, emailAndPassword: { enabled: true, requireEmailVerification: false, autoSignIn: false } }); ``` ### Additional context Without the additional config it works fine. My DB config is as such ``` export const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: env.DATABASE_URL, max: 10 }) }), plugins: [new CamelCasePlugin({ underscoreBeforeDigits: true })] }); ``` Migrations say this, so that is at least working: ``` ⠋ preparing migration...2025-06-28T20:03:16.359Z WARN [Better Auth]: Field name in table user has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field email in table user has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field image in table user has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field token in table session has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field ip_address in table session has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field user_agent in table session has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field user_id in table session has a different type in the database. Expected string but got uuid. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field account_id in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field provider_id in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field user_id in table account has a different type in the database. Expected string but got uuid. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field access_token in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field refresh_token in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field id_token in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field scope in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field password in table account has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field identifier in table verification has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.359Z WARN [Better Auth]: Field value in table verification has a different type in the database. Expected string but got varchar. 2025-06-28T20:03:16.360Z INFO [Better Auth]: 🚀 No migrations needed. ``` I suspect it's to do with my snakecase'd columns, but without the custom table mappings it works correctly. The Kysely plugins approach seems clearer to me. More transparent in terms of magic, less transparent in terms of explicitness
GiteaMirror added the bug label 2026-03-13 08:40:16 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jun 28, 2025):

The root cause is a mismatch between the keys returned by Kysely (with CamelCasePlugin) and what Better Auth expects when custom field mappings are used. With CamelCasePlugin, Kysely returns camelCase keys (e.g., emailVerified), but your Better Auth config maps logical fields (emailVerified) to snake_case DB columns (email_verified). When Better Auth tries to map DB results back to logical fields, it looks for snake_case keys (email_verified) in the result, but Kysely has already converted them to camelCase (emailVerified), so the values end up undefined in the parsed result.

This is a known subtlety when combining custom field mappings and Kysely's CamelCasePlugin. The recommended pattern is to use snake_case columns in the DB, CamelCasePlugin in Kysely, and map fields in Better Auth, but the mapping config and the actual DB result keys must align see discussion.

To fix this, you can either:

  • Remove the custom field mappings from your Better Auth config and rely on CamelCasePlugin to handle the mapping (if your logical and DB field names match after camelCasing).
  • Or, if you need custom mappings, use Better Auth's customTransformOutput to remap the keys from camelCase (as returned by Kysely) to the logical field names expected by Better Auth.

If you want to keep both custom mappings and CamelCasePlugin, a minimal workaround is to add a customTransformOutput function in your Better Auth config that remaps the camelCase keys to the logical field names. This bridges the gap between Kysely's output and Better Auth's expectations see code reference.

This pattern is supported and stable, but requires careful alignment between your DB schema, Kysely config, and Better Auth field mappings see also. If you want a concrete example or further troubleshooting steps, let me know!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

@dosubot[bot] commented on GitHub (Jun 28, 2025): <!-- Answer --> The root cause is a mismatch between the keys returned by Kysely (with CamelCasePlugin) and what Better Auth expects when custom field mappings are used. With CamelCasePlugin, Kysely returns camelCase keys (e.g., emailVerified), but your Better Auth config maps logical fields (emailVerified) to snake_case DB columns (email_verified). When Better Auth tries to map DB results back to logical fields, it looks for snake_case keys (email_verified) in the result, but Kysely has already converted them to camelCase (emailVerified), so the values end up undefined in the parsed result. This is a known subtlety when combining custom field mappings and Kysely's CamelCasePlugin. The recommended pattern is to use snake_case columns in the DB, CamelCasePlugin in Kysely, and map fields in Better Auth, but the mapping config and the actual DB result keys must align [see discussion](https://github.com/better-auth/better-auth/issues/3191). To fix this, you can either: - Remove the custom field mappings from your Better Auth config and rely on CamelCasePlugin to handle the mapping (if your logical and DB field names match after camelCasing). - Or, if you need custom mappings, use Better Auth's customTransformOutput to remap the keys from camelCase (as returned by Kysely) to the logical field names expected by Better Auth. If you want to keep both custom mappings and CamelCasePlugin, a minimal workaround is to add a customTransformOutput function in your Better Auth config that remaps the camelCase keys to the logical field names. This bridges the gap between Kysely's output and Better Auth's expectations [see code reference](https://github.com/better-auth/better-auth/blob/e7eed8c848740fc22dc12e1b82cad8a42155249e/packages/better-auth/src/adapters/create-adapter/index.ts). This pattern is supported and stable, but requires careful alignment between your DB schema, Kysely config, and Better Auth field mappings [see also](https://github.com/better-auth/better-auth/pull/2315). If you want a concrete example or further troubleshooting steps, let me know! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/3aaf92aa-7aed-4d24-9936-4a1934e2175f?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3212)
Author
Owner

@rebasecase commented on GitHub (Jun 29, 2025):

@dosu bad bot. This is not relevant

@rebasecase commented on GitHub (Jun 29, 2025): @dosu bad bot. This is not relevant
Author
Owner

@rebasecase commented on GitHub (Jun 29, 2025):

This also persists in 1.3.0-beta.1

@rebasecase commented on GitHub (Jun 29, 2025): This also persists in 1.3.0-beta.1
Author
Owner

@dosubot[bot] commented on GitHub (Jun 29, 2025):

Sorry about that—looks like I missed what you were actually looking for. Can you clarify what part of your issue you want help with? From your logs and config, I see the problem with undefined fields when using custom mappings and CamelCasePlugin, but if that's not the core issue, let me know what you want to focus on and I'll dig in.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

@dosubot[bot] commented on GitHub (Jun 29, 2025): <!-- Answer --> Sorry about that—looks like I missed what you were actually looking for. Can you clarify what part of your issue you want help with? From your logs and config, I see the problem with undefined fields when using custom mappings and CamelCasePlugin, but if that's not the core issue, let me know what you want to focus on and I'll dig in. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/45e45904-8b69-415f-8bf1-7c9ccacf6bf1?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3212)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1439