[GH-ISSUE #5081] Documentation: Clarify generateId behavior and mixed ID type handling #27482

Closed
opened 2026-04-17 18:31:51 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @elitan on GitHub (Oct 4, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5081

Summary

The current documentation for advanced.database.generateId is unclear about how to handle mixed ID types (e.g., integer IDs for users, UUID strings for other tables). This led to confusion during implementation.

Issues Found

1. Returning false from generateId doesn't work as documented

The documentation suggests you can return false from the generateId function to let the database handle ID generation:

generateId: (options) => {
  if (options.model === 'user') {
    return false; // ❌ This tries to insert "false" as a string!
  }
  return "my-id";
}

Error encountered: error: invalid input syntax for type integer: "false"

Solution: Return undefined instead:

generateId: (options) => {
  if (options.model === 'user' || options.model === 'users') {
    return undefined as any; // ✅ Let database generate the ID
  }
  return crypto.randomUUID();
}

Looking at the source code (better-auth.BrEH5bcK.mjs), the check is:

if (generateId$1 === false || useNumberId) return void 0;

This checks if the config value is false, not if the function returns false.

2. Mixed ID Types Not Documented

Scenario: Users table with PostgreSQL serial (integer) IDs, but session/account/verification tables with text (UUID) IDs.

The documentation doesn't explain:

  • That useNumberId is a global setting affecting all tables
  • How to handle mixed ID types across different tables
  • That you should NOT use useNumberId when mixing types

Suggested Documentation Improvements

1. Update advanced.database.generateId section

database: {
  // Option 1: Disable ID generation for ALL tables
  generateId: false,
  
  // Option 2: Custom ID generation per model
  generateId: (options) => {
    // Let database auto-generate (for serial, auto_increment, etc.)
    if (options.model === 'user' || options.model === 'users') {
      return undefined as any;
    }
    
    // Generate UUIDs for other tables
    return crypto.randomUUID();
  },
}

Important: To let the database generate IDs, return undefined from the function. Setting generateId: false disables generation for ALL tables.

2. Add Mixed ID Types Example

// Example: Integer IDs for users, UUID strings for other tables
export const auth = betterAuth({
  user: {
    modelName: 'users', // PostgreSQL: id serial primary key
  },
  session: {
    modelName: 'session', // PostgreSQL: id text primary key
  },
  advanced: {
    database: {
      // Do NOT set useNumberId - it's global and affects all tables
      generateId: (options) => {
        if (options.model === 'user' || options.model === 'users') {
          return undefined as any; // Let PostgreSQL serial generate it
        }
        return crypto.randomUUID(); // UUIDs for session, account, verification
      },
    },
  },
})

3. Document additionalFields.returned property

user: {
  additionalFields: {
    firstName: {
      type: 'string',
      fieldName: 'first_name',
      returned: true, // ✅ Required to include field in SELECT queries
    },
  },
}

Without returned: true, fields are stored but not retrieved from the database.

4. Clarify Database Hooks Naming

databaseHooks: {
  user: {
    create: {
      before: async (user) => {
        // ✅ Use JavaScript property names, not database column names
        return {
          data: {
            ...user,
            firstName: 'John', // Better Auth maps this to first_name
          },
        };
      },
    },
  },
}

Additional Context

  • Database: PostgreSQL with Kysely adapter
  • Use case: Migrating from Clerk to better-auth with existing schema
  • Constraint: Cannot change users table from integer to text IDs (many dependencies)

Would be happy to submit a PR with these documentation improvements if that would be helpful

Originally created by @elitan on GitHub (Oct 4, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5081 ## Summary The current documentation for `advanced.database.generateId` is unclear about how to handle mixed ID types (e.g., integer IDs for users, UUID strings for other tables). This led to confusion during implementation. ## Issues Found ### 1. Returning `false` from generateId doesn't work as documented The documentation suggests you can return `false` from the `generateId` function to let the database handle ID generation: ```ts generateId: (options) => { if (options.model === 'user') { return false; // ❌ This tries to insert "false" as a string! } return "my-id"; } ``` **Error encountered:** `error: invalid input syntax for type integer: "false"` **Solution:** Return `undefined` instead: ```ts generateId: (options) => { if (options.model === 'user' || options.model === 'users') { return undefined as any; // ✅ Let database generate the ID } return crypto.randomUUID(); } ``` Looking at the source code (`better-auth.BrEH5bcK.mjs`), the check is: ```js if (generateId$1 === false || useNumberId) return void 0; ``` This checks if the *config value* is `false`, not if the *function returns* `false`. ### 2. Mixed ID Types Not Documented **Scenario:** Users table with PostgreSQL `serial` (integer) IDs, but session/account/verification tables with `text` (UUID) IDs. The documentation doesn't explain: - That `useNumberId` is a global setting affecting all tables - How to handle mixed ID types across different tables - That you should NOT use `useNumberId` when mixing types ## Suggested Documentation Improvements ### 1. Update `advanced.database.generateId` section ```ts database: { // Option 1: Disable ID generation for ALL tables generateId: false, // Option 2: Custom ID generation per model generateId: (options) => { // Let database auto-generate (for serial, auto_increment, etc.) if (options.model === 'user' || options.model === 'users') { return undefined as any; } // Generate UUIDs for other tables return crypto.randomUUID(); }, } ``` **Important:** To let the database generate IDs, return `undefined` from the function. Setting `generateId: false` disables generation for ALL tables. ### 2. Add Mixed ID Types Example ```ts // Example: Integer IDs for users, UUID strings for other tables export const auth = betterAuth({ user: { modelName: 'users', // PostgreSQL: id serial primary key }, session: { modelName: 'session', // PostgreSQL: id text primary key }, advanced: { database: { // Do NOT set useNumberId - it's global and affects all tables generateId: (options) => { if (options.model === 'user' || options.model === 'users') { return undefined as any; // Let PostgreSQL serial generate it } return crypto.randomUUID(); // UUIDs for session, account, verification }, }, }, }) ``` ### 3. Document `additionalFields.returned` property ```ts user: { additionalFields: { firstName: { type: 'string', fieldName: 'first_name', returned: true, // ✅ Required to include field in SELECT queries }, }, } ``` Without `returned: true`, fields are stored but not retrieved from the database. ### 4. Clarify Database Hooks Naming ```ts databaseHooks: { user: { create: { before: async (user) => { // ✅ Use JavaScript property names, not database column names return { data: { ...user, firstName: 'John', // Better Auth maps this to first_name }, }; }, }, }, } ``` ## Additional Context - Database: PostgreSQL with Kysely adapter - Use case: Migrating from Clerk to better-auth with existing schema - Constraint: Cannot change users table from integer to text IDs (many dependencies) Would be happy to submit a PR with these documentation improvements if that would be helpful
GiteaMirror added the locked label 2026-04-17 18:31:51 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 4, 2025):

Hi @elitan! I'm Dosu and I’m helping the better-auth team.

Here’s the current, correct behavior based on the latest code and merged PRs:

  • Returning false from the generateId function is now officially supported and will let the database generate the ID (e.g., for serial columns in PostgreSQL). This was formalized in a recent update, so you no longer need to return undefined—either works, but false is now the recommended, documented way for this use case (source).

  • The useNumberId option is global and affects all tables. It should not be used if you need mixed ID types (e.g., integer for users, UUID for sessions/accounts), as it disables internal ID generation and expects all IDs to be numbers (source).

  • For mixed ID types, use a generateId function that returns false (or undefined) for models where the database should generate the ID (like users with serial integer IDs), and returns a UUID for others. Example:

    generateId: (options) => {
      if (options.model === 'user' || options.model === 'users') {
        return false; // Let the DB handle serial integer IDs
      }
      return crypto.randomUUID(); // UUIDs for other tables
    }
    
  • Setting generateId: false disables ID generation for all tables globally, which is only appropriate if your DB handles all IDs (not for mixed types) (source).

  • The additionalFields.returned property controls whether a custom field is included in SELECT/read queries. If omitted or set to false, the field is stored but not retrieved [(note)].

  • In databaseHooks, always use JavaScript property names (e.g., firstName), not DB column names (e.g., first_name). The mapping is handled internally [(note)].

Your suggested documentation improvements are accurate and align with the current implementation. The only update is that returning false from generateId is now the preferred way to delegate to the database, and this should be reflected in examples and docs. There are several related issues and discussions confirming these behaviors and the need for clearer documentation (source) (source).

To reply, just mention @dosu.


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

<!-- gh-comment-id:3368171021 --> @dosubot[bot] commented on GitHub (Oct 4, 2025): <!-- Greeting --> Hi @elitan! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Here’s the current, correct behavior based on the latest code and merged PRs: - Returning `false` from the `generateId` function is now officially supported and will let the database generate the ID (e.g., for serial columns in PostgreSQL). This was formalized in a recent update, so you no longer need to return `undefined`—either works, but `false` is now the recommended, documented way for this use case [(source)](https://github.com/better-auth/better-auth/pull/3723). - The `useNumberId` option is global and affects all tables. It should not be used if you need mixed ID types (e.g., integer for users, UUID for sessions/accounts), as it disables internal ID generation and expects all IDs to be numbers [(source)](https://github.com/better-auth/better-auth/issues/2420). - For mixed ID types, use a `generateId` function that returns `false` (or `undefined`) for models where the database should generate the ID (like users with serial integer IDs), and returns a UUID for others. Example: ```ts generateId: (options) => { if (options.model === 'user' || options.model === 'users') { return false; // Let the DB handle serial integer IDs } return crypto.randomUUID(); // UUIDs for other tables } ``` - Setting `generateId: false` disables ID generation for all tables globally, which is only appropriate if your DB handles all IDs (not for mixed types) [(source)](https://github.com/better-auth/better-auth/issues/2420). - The `additionalFields.returned` property controls whether a custom field is included in SELECT/read queries. If omitted or set to `false`, the field is stored but not retrieved [(note)]. - In `databaseHooks`, always use JavaScript property names (e.g., `firstName`), not DB column names (e.g., `first_name`). The mapping is handled internally [(note)]. Your suggested documentation improvements are accurate and align with the current implementation. The only update is that returning `false` from `generateId` is now the preferred way to delegate to the database, and this should be reflected in examples and docs. There are several related issues and discussions confirming these behaviors and the need for clearer documentation [(source)](https://github.com/better-auth/better-auth/issues/2420) [(source)](https://github.com/better-auth/better-auth/issues/2881). <!-- 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/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/95490b73-5216-4ac2-9a73-d0ea7945f9cc?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/5081)
Author
Owner

@michaelbironneau commented on GitHub (Oct 5, 2025):

Yes, and the docs could do with clarifying the required database schema, specifically the id column of the user table. Using the better-auth CLI to generate the schema will lead to a numerical (serial, auto-incrementing) id column whereas strings are required if you roll your own. See e.g. #2349 and #3450.

<!-- gh-comment-id:3368879314 --> @michaelbironneau commented on GitHub (Oct 5, 2025): Yes, and the docs could do with clarifying the required database schema, specifically the `id` column of the user table. Using the better-auth CLI to generate the schema will lead to a numerical (serial, auto-incrementing) `id` column whereas strings are required if you roll your own. See e.g. #2349 and #3450.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27482