[GH-ISSUE #3190] Allow custom type of users id in admin plugin - unable to impersonate user #9507

Closed
opened 2026-04-13 04:59:50 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @ismi-abbas on GitHub (Jun 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3190

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Ex:

  1. Implement with curent existed user table with generateId = false and id is type of number.
  2. Using admin plugin to pass user id to impersonate.
  3. Types expecting string instead of number

advanced: { database: { generateId: false, }, },

const result = await auth.api.impersonateUser({ body: { userId: userToImpersonate.id, }, });
Type 'number' is not assignable to type 'string'.ts(2322)
index.d.ts(720, 21): The expected type comes from property 'userId' which is declared here on type '{ userId: string; }'

export const users = table("users", {
  id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
  googleAuthId: t.text("google_auth_id"),
  employeeId: t.text("employee_id"),
  name: t.text("name"),
  email: t.text("email").unique(),
  jobTitle: t.text("job_title"),
  emailVerified: t
    .boolean("email_verified")
    .$defaultFn(() => false)
    .notNull(),
  image: t.text("image"),
  phoneNumber: t.text("phone_number").unique(),
  phoneNumberVerified: t.boolean("phone_number_verified"),
  role: t.text("role"),
  isActive: t.boolean("is_active").default(true).notNull(),
  departmentId: t.integer("department_id").references(() => departments.id),
  branchId: t.integer("branch_id").references(() => branches.id),
  oldId: t.integer("old_id"),
  deskNo: t.text("desk_no"),
  createdAt: t.timestamp("created_at").defaultNow().notNull(),
  updatedAt: t.timestamp("updated_at").defaultNow().notNull(),
  username: t.text("username").unique(),
  displayUsername: t.text("display_username"),
  banned: t.boolean("banned"),
  banReason: t.text("ban_reason"),
  banExpires: t.timestamp("ban_expires"),
});

Current vs. Expected behavior

The admin plugin should infer the database types / allow to customize the types

What version of Better Auth are you using?

1.2.10

Provide environment information

Chrome

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import * as schema from "@/db/schema/auth/better-auth";
import { users as usersSchema } from "@/db/schema";
import {
  phoneNumber,
  admin as adminPlugin,
  username,
} from "better-auth/plugins";
import { nextCookies } from "better-auth/next-js";
import { ac, betterAuthRoles } from "./users/permissions";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      ...schema,
      users: usersSchema,
    },
  }),
  advanced: {
    database: {
      generateId: false,
    },
  },
  accountLinking: true,
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    disableSignUp: true,
  },
  plugins: [
    username(),
    nextCookies(),
    phoneNumber(),
    adminPlugin({
      adminRoles: ["developer"],
      ac,
      roles: betterAuthRoles,
    }),
  ],
  socialProviders: {
    google: {
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
      scope: ["openid", "email", "profile"],
      mapProfileToUser: (profile) => {
        return {
          googleAuthId: profile.sub,
          email: profile.email,
          name: profile.name,
          image: profile.picture,
          emailVerified: profile.email_verified,
        };
      },
    },
  },
  session: {
    modelName: "sessions",
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60,
    },
    expiresIn: 60 * 60 * 24 * 7,
    updateAge: 60 * 60 * 24,
  },
  user: {
    modelName: "users",
    additionalFields: {
      employeeId: {
        type: "string",
        required: false,
      },
      departmentId: {
        type: "number",
        required: false,
      },
      branchId: {
        type: "number",
        required: false,
      },
      oldId: {
        type: "number",
        required: false,
      },
      role: {
        type: "string",
        required: false,
      },
      isActive: {
        type: "boolean",
        required: false,
        defaultValue: true,
      },
    },
  },
  account: {
    modelName: "accounts",
  },
  verification: {
    modelName: "verifications",
  },
});

export type Session = typeof auth.$Infer.Session;

Additional context

No response

Originally created by @ismi-abbas on GitHub (Jun 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3190 ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce Ex: 1. Implement with curent existed user table with generateId = false and id is type of number. 2. Using admin plugin to pass user id to impersonate. 3. Types expecting string instead of number ` advanced: { database: { generateId: false, }, },` ` const result = await auth.api.impersonateUser({ body: { userId: userToImpersonate.id, }, }); ` Type 'number' is not assignable to type 'string'.ts(2322) index.d.ts(720, 21): The expected type comes from property 'userId' which is declared here on type '{ userId: string; }' ```ts export const users = table("users", { id: t.integer().primaryKey().generatedAlwaysAsIdentity(), googleAuthId: t.text("google_auth_id"), employeeId: t.text("employee_id"), name: t.text("name"), email: t.text("email").unique(), jobTitle: t.text("job_title"), emailVerified: t .boolean("email_verified") .$defaultFn(() => false) .notNull(), image: t.text("image"), phoneNumber: t.text("phone_number").unique(), phoneNumberVerified: t.boolean("phone_number_verified"), role: t.text("role"), isActive: t.boolean("is_active").default(true).notNull(), departmentId: t.integer("department_id").references(() => departments.id), branchId: t.integer("branch_id").references(() => branches.id), oldId: t.integer("old_id"), deskNo: t.text("desk_no"), createdAt: t.timestamp("created_at").defaultNow().notNull(), updatedAt: t.timestamp("updated_at").defaultNow().notNull(), username: t.text("username").unique(), displayUsername: t.text("display_username"), banned: t.boolean("banned"), banReason: t.text("ban_reason"), banExpires: t.timestamp("ban_expires"), }); ``` ### Current vs. Expected behavior The admin plugin should infer the database types / allow to customize the types ### What version of Better Auth are you using? 1.2.10 ### Provide environment information ```bash Chrome ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/db"; import * as schema from "@/db/schema/auth/better-auth"; import { users as usersSchema } from "@/db/schema"; import { phoneNumber, admin as adminPlugin, username, } from "better-auth/plugins"; import { nextCookies } from "better-auth/next-js"; import { ac, betterAuthRoles } from "./users/permissions"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: { ...schema, users: usersSchema, }, }), advanced: { database: { generateId: false, }, }, accountLinking: true, emailAndPassword: { enabled: true, autoSignIn: true, disableSignUp: true, }, plugins: [ username(), nextCookies(), phoneNumber(), adminPlugin({ adminRoles: ["developer"], ac, roles: betterAuthRoles, }), ], socialProviders: { google: { clientId: process.env.AUTH_GOOGLE_ID!, clientSecret: process.env.AUTH_GOOGLE_SECRET!, scope: ["openid", "email", "profile"], mapProfileToUser: (profile) => { return { googleAuthId: profile.sub, email: profile.email, name: profile.name, image: profile.picture, emailVerified: profile.email_verified, }; }, }, }, session: { modelName: "sessions", cookieCache: { enabled: true, maxAge: 5 * 60, }, expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24, }, user: { modelName: "users", additionalFields: { employeeId: { type: "string", required: false, }, departmentId: { type: "number", required: false, }, branchId: { type: "number", required: false, }, oldId: { type: "number", required: false, }, role: { type: "string", required: false, }, isActive: { type: "boolean", required: false, defaultValue: true, }, }, }, account: { modelName: "accounts", }, verification: { modelName: "verifications", }, }); export type Session = typeof auth.$Infer.Session; ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 04:59:50 -05:00
Author
Owner

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

You may be able to use the following in your config

advanced: {
  database: {
    generateId: false
  }
}

but this may still be a string - I've not tested it.

I believe this is what you want:

advanced: {
  database: {
    useNumberId: true
  }
}

This is a work around for your actual problem. It would be good to be able to provide any type for the ID.

<!-- gh-comment-id:3014261157 --> @rebasecase commented on GitHub (Jun 27, 2025): You may be able to use the following in your config ``` advanced: { database: { generateId: false } } ``` but this may still be a string - I've not tested it. I believe this is what you want: ``` advanced: { database: { useNumberId: true } } ``` This is a work around for your actual problem. It would be good to be able to provide any type for the ID.
Author
Owner

@ismi-abbas commented on GitHub (Jun 30, 2025):

That still didn't infer the id type. Hence this api also not inferring the id type as string

const session = await auth.api.getSession({
  headers: headers(),
});

// types inferred 
const session: {
    session: {
        id: string;
        token: string;
        userId: string;
        expiresAt: Date;
        createdAt: Date;
        updatedAt: Date;
        ipAddress?: string | null | undefined | undefined;
        userAgent?: string | null | undefined | undefined;
        impersonatedBy?: string | ... 1 more ... | undefined;
    };
<!-- gh-comment-id:3018464989 --> @ismi-abbas commented on GitHub (Jun 30, 2025): That still didn't infer the id type. Hence this api also not inferring the id type as string ```ts const session = await auth.api.getSession({ headers: headers(), }); // types inferred const session: { session: { id: string; token: string; userId: string; expiresAt: Date; createdAt: Date; updatedAt: Date; ipAddress?: string | null | undefined | undefined; userAgent?: string | null | undefined | undefined; impersonatedBy?: string | ... 1 more ... | undefined; }; ```
Author
Owner

@ping-maxwell commented on GitHub (Jul 1, 2025):

@ismi-abbas It's by design that the types remain the same, it's important that it's like this so that all Better-auth plugins and libraries will work the same without breaking.
Our internal adapter system will automatically translate the data accordingly.

<!-- gh-comment-id:3021290774 --> @ping-maxwell commented on GitHub (Jul 1, 2025): @ismi-abbas It's by design that the types remain the same, it's important that it's like this so that all Better-auth plugins and libraries will work the same without breaking. Our internal adapter system will automatically translate the data accordingly.
Author
Owner

@szilardx commented on GitHub (Jul 6, 2025):

@ping-maxwell So if I must use useNumberId: true there is no chance to use api.userHasPermission call?

I was using this for permission check, all was fine, until I had to turn on numericId-s for external app compatibility.
Since then I get

sqlState: '42S22',
  sqlMessage: "Unknown column 'NaN' in 'WHERE'",
  sql: 'select * from `user` where `id` = NaN',
<!-- gh-comment-id:3042032358 --> @szilardx commented on GitHub (Jul 6, 2025): @ping-maxwell So if I must use `useNumberId: true` there is no chance to use api.userHasPermission call? I was using this for permission check, all was fine, until I had to turn on numericId-s for external app compatibility. Since then I get ``` sqlState: '42S22', sqlMessage: "Unknown column 'NaN' in 'WHERE'", sql: 'select * from `user` where `id` = NaN', ```
Author
Owner

@ping-maxwell commented on GitHub (Jul 6, 2025):

@szilardx That looks like an unexpected bug, please open a separate issue for this.

<!-- gh-comment-id:3042155093 --> @ping-maxwell commented on GitHub (Jul 6, 2025): @szilardx That looks like an unexpected bug, please open a separate issue for this.
Author
Owner

@rebasecase commented on GitHub (Jul 6, 2025):

I would not expect NaN to propagate this far. Certainly not injected into the query like that

<!-- gh-comment-id:3042165809 --> @rebasecase commented on GitHub (Jul 6, 2025): I would not expect NaN to propagate this far. Certainly not injected into the query like that
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9507