[GH-ISSUE #9056] databaseHooks.user.create.before cannot override user ID when generateId: "uuid" is used with PostgreSQL #19892

Closed
opened 2026-04-15 19:15:18 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @hrougier on GitHub (Apr 9, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9056

Originally assigned to: @ping-maxwell on GitHub.

Is this suited for github?

  • Yes, this is suited for github

Reproduction

  1. Configure better-auth with generateId: "uuid", a pg Pool, and a databaseHooks.user.create.before hook that returns a custom ID:
export const auth = betterAuth({
  database: new Pool({ connectionString: process.env.DATABASE_URL }),
  advanced: {
    database: { generateId: "uuid" },
  },
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          // Try to reuse an existing ID from another table
          const existingId = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
          return { data: { ...user, id: existingId } }
        },
      },
    },
  },
})
  1. Call auth.api.signUpEmail({ body: { email: "test@example.com", password: "password123", name: "Test" } })
  2. Observe that the created user has a randomly generated UUID, not the ID returned by the before hook.

Current vs. Expected behavior

Expected: The before hook returns { data: { ...user, id: existingId } }, and the created user record uses existingId as its primary key. The with-hooks.mjs layer correctly passes forceAllowId: true to the adapter for this purpose.

Actual: The custom ID is silently discarded. The user record gets a database-generated UUID instead.

Root cause: In @better-auth/core/dist/db/adapter/get-id-field.mjs, the transform.input function has a guard ordering bug:

// line 40-50                                                                                                                                                               
if (useUUIDs) {                                                                                                                                                             
  if (shouldGenerateId && !forceAllowId) return value;
  if (disableIdGeneration) return void 0;                                                                                                                                   
  if (supportsUUIDs) return void 0;           // ← line 43: returns undefined BEFORE forceAllowId is checked                                                                
  if (forceAllowId && typeof value === "string") // ← line 44: never reached when supportsUUIDs=true                                                                        
    if (uuidRegex.test(value)) return value;                                                                                                                                
  // ...                                                                                                                                                                    
}

When using PostgreSQL, supportsUUIDs is true (set by the kysely adapter). Line 43 (if (supportsUUIDs) return void 0) fires before line 44 (if (forceAllowId && ...) return value), so the custom ID is thrown away and the database's DEFAULT gen_random_uuid() takes over — regardless of forceAllowId.

Could the forceAllowId check on line 44 run before the supportsUUIDs guard on line 43?

if (useUUIDs) {                                                                                                                                                             
  if (shouldGenerateId && !forceAllowId) return value;
  if (disableIdGeneration) return void 0;
  if (forceAllowId && typeof value === "string")  // ← check forceAllowId FIRST                                                                                             
    if (uuidRegex.test(value)) return value;
  if (supportsUUIDs) return void 0;               // ← then fall through to DB default                                                                                      
  // ...                                      
}

What version of Better Auth are you using?

1.6.0

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64"                                                                                                                                                         
  },
  "node": {                                                                                                                                                                 
    "version": "v24.11.1"                     
  },
  "frameworks": [
    { "name": "next", "version": "16.2.2" },
    { "name": "react", "version": "19.2.4" }                                                                                                                                
  ],
  "databases": [                                                                                                                                                            
    { "name": "pg", "version": "8.20.0" }     
  ],
  "betterAuth": {
    "version": "1.6.0"                                                                                                                                                      
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
import { Pool } from "pg"

export const auth = betterAuth({
  database: new Pool({
    connectionString: process.env.DATABASE_URL,
  }),
  advanced: {
    database: {
      generateId: "uuid",
    },
  },
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          // Look up an existing record by email to reuse its UUID
          const existingId = await getExistingUserId(user.email)
          if (existingId) {
            return { data: { ...user, id: existingId } }
          }
          return { data: user }
        },
      },
    },
  },
})

Additional context

  • The bug is in @better-auth/core/dist/db/adapter/get-id-field.mjs (the transform.input closure), not in better-auth itself.
  • Only affects databases where supportsUUIDs is true (PostgreSQL via kysely, drizzle, or prisma adapters). SQLite/MySQL are not affected.
Originally created by @hrougier on GitHub (Apr 9, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9056 Originally assigned to: @ping-maxwell on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### Reproduction 1. Configure better-auth with `generateId: "uuid"`, a `pg` Pool, and a `databaseHooks.user.create.before` hook that returns a custom ID: ```ts export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL }), advanced: { database: { generateId: "uuid" }, }, databaseHooks: { user: { create: { before: async (user) => { // Try to reuse an existing ID from another table const existingId = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" return { data: { ...user, id: existingId } } }, }, }, }, }) ``` 2. Call `auth.api.signUpEmail({ body: { email: "test@example.com", password: "password123", name: "Test" } })` 3. Observe that the created user has a randomly generated UUID, not the ID returned by the `before` hook. ### Current vs. Expected behavior Expected: The before hook returns `{ data: { ...user, id: existingId } }`, and the created user record uses `existingId` as its primary key. The `with-hooks.mjs` layer correctly passes `forceAllowId: true` to the adapter for this purpose. Actual: The custom ID is silently discarded. The user record gets a database-generated UUID instead. Root cause: In `@better-auth/core/dist/db/adapter/get-id-field.mjs`, the `transform.input` function has a guard ordering bug: ```ts // line 40-50 if (useUUIDs) { if (shouldGenerateId && !forceAllowId) return value; if (disableIdGeneration) return void 0; if (supportsUUIDs) return void 0; // ← line 43: returns undefined BEFORE forceAllowId is checked if (forceAllowId && typeof value === "string") // ← line 44: never reached when supportsUUIDs=true if (uuidRegex.test(value)) return value; // ... } ``` When using PostgreSQL, `supportsUUIDs` is `true` (set by the kysely adapter). Line 43 (`if (supportsUUIDs) return void 0`) fires before line 44 (`if (forceAllowId && ...) return value`), so the custom ID is thrown away and the database's `DEFAULT gen_random_uuid()` takes over — regardless of `forceAllowId`. Could the `forceAllowId` check on line 44 run before the `supportsUUIDs` guard on line 43? ```ts if (useUUIDs) { if (shouldGenerateId && !forceAllowId) return value; if (disableIdGeneration) return void 0; if (forceAllowId && typeof value === "string") // ← check forceAllowId FIRST if (uuidRegex.test(value)) return value; if (supportsUUIDs) return void 0; // ← then fall through to DB default // ... } ``` ### What version of Better Auth are you using? 1.6.0 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64" }, "node": { "version": "v24.11.1" }, "frameworks": [ { "name": "next", "version": "16.2.2" }, { "name": "react", "version": "19.2.4" } ], "databases": [ { "name": "pg", "version": "8.20.0" } ], "betterAuth": { "version": "1.6.0" } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" import { Pool } from "pg" export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), advanced: { database: { generateId: "uuid", }, }, databaseHooks: { user: { create: { before: async (user) => { // Look up an existing record by email to reuse its UUID const existingId = await getExistingUserId(user.email) if (existingId) { return { data: { ...user, id: existingId } } } return { data: user } }, }, }, }, }) ``` ### Additional context - The bug is in `@better-auth/core/dist/db/adapter/get-id-field.mjs` (the `transform.input` closure), not in better-auth itself. - Only affects databases where `supportsUUIDs` is `true` (PostgreSQL via kysely, drizzle, or prisma adapters). SQLite/MySQL are not affected.
GiteaMirror added the database label 2026-04-15 19:15:18 -05:00
Author
Owner

@GautamBytes commented on GitHub (Apr 9, 2026):

Looking into it!

<!-- gh-comment-id:4215132030 --> @GautamBytes commented on GitHub (Apr 9, 2026): Looking into it!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19892