[GH-ISSUE #3069] [Organizations] usePlural doesn't work with custom schema name #9459

Closed
opened 2026-04-13 04:55:54 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @ctrhub on GitHub (Jun 18, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3069

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. set usePlural to true
  2. change default organizations model names:
plugins: [
  organization({
    schema: {
      invitation: {
        // note that the changed model name is not plural, otherwise usePlural adds an extra "s"
        // 'organization_invitations' -> 'organization_invitationss'(in drizzle schema)
  	modelName: 'organization_invitation'      },
      member: {
      	modelName: 'organization_member'
      },
    }
  }),
]
  1. use auth.api.createOrganization to get the error below:
Error updating user:  [BetterAuthError: [# Drizzle Adapter]: The model "organization_member" was not found in the schema object. Please pass the schema directly to the adapter options.] {
  cause: undefined
}

Current vs. Expected behavior

Current schema generation and migration works as expected.
But Drizzle Adapter seems to fail to pickup these changes.

What version of Better Auth are you using?

1.2.8

Provide environment information

- MacOS (Sequoia 15.0)
- Arc 1.99.0

Packages
- drizzle-kit 0.31.1
- @better-auth/cli 1.2.9
- better-auth 1.2.9

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

Package

Auth config (if applicable)

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      ...betterAuthSchemas,
    },
    usePlural: true,
  }),

  plugins: [
    organization({
      schema: {
        invitation: {
          modelName: "organization_invitation",
        },
        member: {
          modelName: "organization_member",
        },
      },
    }),
  ],
});

Additional context

Drizzle schema generate by better-auth:

export const organizations = pgTable("organizations", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  slug: text("slug").unique(),
  logo: text("logo"),
  createdAt: timestamp("created_at").notNull(),
  metadata: text("metadata"),
});

export const organization_members = pgTable("organization_members", {
  id: text("id").primaryKey(),
  organizationId: text("organization_id")
    .notNull()
    .references(() => organizations.id, { onDelete: "cascade" }),
  userId: text("user_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  role: text("role").default("member").notNull(),
  createdAt: timestamp("created_at").notNull(),
});

export const organization_invitations = pgTable("organization_invitations", {
  id: text("id").primaryKey(),
  organizationId: text("organization_id")
    .notNull()
    .references(() => organizations.id, { onDelete: "cascade" }),
  email: text("email").notNull(),
  role: text("role"),
  status: text("status").default("pending").notNull(),
  expiresAt: timestamp("expires_at").notNull(),
  inviterId: text("inviter_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
});

Originally created by @ctrhub on GitHub (Jun 18, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3069 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. set `usePlural` to `true` 2. change default organizations model names: ```JS plugins: [ organization({ schema: { invitation: { // note that the changed model name is not plural, otherwise usePlural adds an extra "s" // 'organization_invitations' -> 'organization_invitationss'(in drizzle schema) modelName: 'organization_invitation' }, member: { modelName: 'organization_member' }, } }), ] ``` 3. use `auth.api.createOrganization` to get the error below: ``` Error updating user: [BetterAuthError: [# Drizzle Adapter]: The model "organization_member" was not found in the schema object. Please pass the schema directly to the adapter options.] { cause: undefined } ``` ### Current vs. Expected behavior Current schema generation and migration works as expected. But Drizzle Adapter seems to fail to pickup these changes. ### What version of Better Auth are you using? 1.2.8 ### Provide environment information ```bash - MacOS (Sequoia 15.0) - Arc 1.99.0 Packages - drizzle-kit 0.31.1 - @better-auth/cli 1.2.9 - better-auth 1.2.9 ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: { ...betterAuthSchemas, }, usePlural: true, }), plugins: [ organization({ schema: { invitation: { modelName: "organization_invitation", }, member: { modelName: "organization_member", }, }, }), ], }); ``` ### Additional context Drizzle schema generate by better-auth: ```JS export const organizations = pgTable("organizations", { id: text("id").primaryKey(), name: text("name").notNull(), slug: text("slug").unique(), logo: text("logo"), createdAt: timestamp("created_at").notNull(), metadata: text("metadata"), }); export const organization_members = pgTable("organization_members", { id: text("id").primaryKey(), organizationId: text("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), role: text("role").default("member").notNull(), createdAt: timestamp("created_at").notNull(), }); export const organization_invitations = pgTable("organization_invitations", { id: text("id").primaryKey(), organizationId: text("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), email: text("email").notNull(), role: text("role"), status: text("status").default("pending").notNull(), expiresAt: timestamp("expires_at").notNull(), inviterId: text("inviter_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), }); ```
GiteaMirror added the locked label 2026-04-13 04:55:54 -05:00
Author
Owner

@ctrhub commented on GitHub (Jun 18, 2025):

If I remove usePlural but leave the model names plural, the Drizzle schema will be generated with invalid references:

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      ...betterAuthSchemas,
    },
    // usePlural: true,
  }),

  user: {
    modelName: "users",
    additionalFields: {
      lastName: {
        type: "string",
        required: false,
      },
      isOnboarded: {
        type: "boolean",
        required: false,
        defaultValue: false,
      },
    },
  },
  verification: {
    modelName: "verifications",
  },
  session: {
    modelName: "sessions",
  },
  account: {
    modelName: "accounts",
  },

  plugins: [
    organization({
      schema: {
        organization: {
          modelName: "organizations",
        },
        invitation: {
          modelName: "organization_invitations",
        },
        member: {
          modelName: "organization_members",
        },
      },
    }),
  ],
});
// Generated schema by better-auth

export const organizations = pgTable("organizations", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  slug: text("slug").unique(),
  logo: text("logo"),
  createdAt: timestamp("created_at").notNull(),
  metadata: text("metadata"),
});

export const organization_members = pgTable("organization_members", {
  id: text("id").primaryKey(),
  organizationId: text("organization_id")
    .notNull()
    // ❗ Note: 'organization' variable doesn't exist, but 'organizations' does
    .references(() => organization.id, { onDelete: "cascade" }),
  userId: text("user_id")
    .notNull()
    // ❗ Note: 'user' variable doesn't exist, but 'users' does
    .references(() => user.id, { onDelete: "cascade" }),
  role: text("role").default("member").notNull(),
  createdAt: timestamp("created_at").notNull(),
});

export const organization_invitations = pgTable("organization_invitations", {
  id: text("id").primaryKey(),
  organizationId: text("organization_id")
    .notNull()
    // ❗ Note: 'organization' variable doesn't exist, but 'organizations' does
    .references(() => organization.id, { onDelete: "cascade" }),
  email: text("email").notNull(),
  role: text("role"),
  status: text("status").default("pending").notNull(),
  expiresAt: timestamp("expires_at").notNull(),
  inviterId: text("inviter_id")
    .notNull()
    // ❗ Note: 'user' variable doesn't exist, but 'users' does
    .references(() => user.id, { onDelete: "cascade" }),
});

Relevant post in Discord - https://discord.com/channels/1288403910284935179/1358552107514331196

<!-- gh-comment-id:2983978071 --> @ctrhub commented on GitHub (Jun 18, 2025): If I remove `usePlural` but leave the model names plural, the Drizzle schema will be generated with invalid references: ```JS export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: { ...betterAuthSchemas, }, // usePlural: true, }), user: { modelName: "users", additionalFields: { lastName: { type: "string", required: false, }, isOnboarded: { type: "boolean", required: false, defaultValue: false, }, }, }, verification: { modelName: "verifications", }, session: { modelName: "sessions", }, account: { modelName: "accounts", }, plugins: [ organization({ schema: { organization: { modelName: "organizations", }, invitation: { modelName: "organization_invitations", }, member: { modelName: "organization_members", }, }, }), ], }); ``` ```JS // Generated schema by better-auth export const organizations = pgTable("organizations", { id: text("id").primaryKey(), name: text("name").notNull(), slug: text("slug").unique(), logo: text("logo"), createdAt: timestamp("created_at").notNull(), metadata: text("metadata"), }); export const organization_members = pgTable("organization_members", { id: text("id").primaryKey(), organizationId: text("organization_id") .notNull() // ❗ Note: 'organization' variable doesn't exist, but 'organizations' does .references(() => organization.id, { onDelete: "cascade" }), userId: text("user_id") .notNull() // ❗ Note: 'user' variable doesn't exist, but 'users' does .references(() => user.id, { onDelete: "cascade" }), role: text("role").default("member").notNull(), createdAt: timestamp("created_at").notNull(), }); export const organization_invitations = pgTable("organization_invitations", { id: text("id").primaryKey(), organizationId: text("organization_id") .notNull() // ❗ Note: 'organization' variable doesn't exist, but 'organizations' does .references(() => organization.id, { onDelete: "cascade" }), email: text("email").notNull(), role: text("role"), status: text("status").default("pending").notNull(), expiresAt: timestamp("expires_at").notNull(), inviterId: text("inviter_id") .notNull() // ❗ Note: 'user' variable doesn't exist, but 'users' does .references(() => user.id, { onDelete: "cascade" }), }); ``` Relevant post in Discord - https://discord.com/channels/1288403910284935179/1358552107514331196
Author
Owner

@ctrhub commented on GitHub (Jun 18, 2025):

after some investigation I noticed:

<!-- gh-comment-id:2984419353 --> @ctrhub commented on GitHub (Jun 18, 2025): after some investigation I noticed: - here: https://github.com/better-auth/better-auth/blob/fd62eba1d0ec71b3abb17ece92a4aae0c3c85270/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts#L65-L70 if i print `console.log('✈️ [# Drizzle Adapter]: getSchema', model, Object.keys(schema));`, this is what I get: ```JS ✈️ [# Drizzle Adapter]: getSchema organization_member [ 'users', 'sessions', 'accounts', 'verifications', 'organizations', 'organization_members', 'organization_invitations' ] Error updating user: [BetterAuthError: [# Drizzle Adapter]: The model "organization_member" was not found in the schema object. Please pass the schema directly to the adapter options.] { cause: undefined } ``` - that adding plural manually to the config initialisation, fixes the issue: ```JS modelName: options?.schema?.member?.modelName + 's', ``` right here: https://github.com/better-auth/better-auth/blob/fd62eba1d0ec71b3abb17ece92a4aae0c3c85270/packages/better-auth/src/plugins/organization/organization.ts#L605-L607
Author
Owner

@ctrhub commented on GitHub (Jun 18, 2025):

i found that getModelName function doesn't take into consideration the usePlural when you use custom model names.
im not sure if this is intended logic, here:

fd62eba1d0/packages/better-auth/src/adapters/create-adapter/index.ts (L156-L167)

you can notice that if the model argument is not equal to the modelName in schema(in case you use custom model name), it returns that custom model name, without modifying it to be plural, if usePlural is set to true.

So, i've updated this function with additional check for the case i mentioned above:

const getModelName = (model) => {
  const defaultModelKey = getDefaultModelName(model);
  const usePlural = config && config.usePlural;
  const useCustomModelName =
    schema &&
    schema[defaultModelKey] &&
    schema[defaultModelKey].modelName !== model;

  if (useCustomModelName) {
    return usePlural
      ? `${schema[defaultModelKey].modelName}s`
      : schema[defaultModelKey].modelName;
  }

  return usePlural ? `${model}s` : model;
};

This basically solved the problem. But I'm not sure if this function is the problem or something else.

<!-- gh-comment-id:2985270520 --> @ctrhub commented on GitHub (Jun 18, 2025): i found that `getModelName` function doesn't take into consideration the `usePlural` when you use custom model names. im not sure if this is intended logic, here: https://github.com/better-auth/better-auth/blob/fd62eba1d0ec71b3abb17ece92a4aae0c3c85270/packages/better-auth/src/adapters/create-adapter/index.ts#L156-L167 you can notice that if the `model` argument is not equal to the `modelName` in schema(in case you use custom model name), it returns that custom model name, without modifying it to be plural, if `usePlural` is set to `true`. So, i've updated this function with additional check for the case i mentioned above: ```JS const getModelName = (model) => { const defaultModelKey = getDefaultModelName(model); const usePlural = config && config.usePlural; const useCustomModelName = schema && schema[defaultModelKey] && schema[defaultModelKey].modelName !== model; if (useCustomModelName) { return usePlural ? `${schema[defaultModelKey].modelName}s` : schema[defaultModelKey].modelName; } return usePlural ? `${model}s` : model; }; ``` This basically solved the problem. But I'm not sure if this function is the problem or something else.
Author
Owner

@ping-maxwell commented on GitHub (Jun 19, 2025):

Hey @ctrhub , good catch!
I'll do some testing on my end and if everything works out I'll open a PR to resolve this.

<!-- gh-comment-id:2987795735 --> @ping-maxwell commented on GitHub (Jun 19, 2025): Hey @ctrhub , good catch! I'll do some testing on my end and if everything works out I'll open a PR to resolve this.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9459