From bae71988ab79aeb4f19f245ceabac9eca8706a50 Mon Sep 17 00:00:00 2001 From: Maxwell <145994855+ping-maxwell@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:58:21 +1000 Subject: [PATCH] fix(organization): apply membershipLimit to listMembers user fetch (#10342) --- ...ix-organization-list-members-user-limit.md | 5 ++ .../src/plugins/organization/adapter.ts | 3 + .../organization/routes/crud-members.test.ts | 82 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 .changeset/fix-organization-list-members-user-limit.md diff --git a/.changeset/fix-organization-list-members-user-limit.md b/.changeset/fix-organization-list-members-user-limit.md new file mode 100644 index 0000000000..da3f7ed467 --- /dev/null +++ b/.changeset/fix-organization-list-members-user-limit.md @@ -0,0 +1,5 @@ +--- +"better-auth": patch +--- + +Fix `organization.listMembers` failing with "User not found for member" for orgs with more than ~100 members by applying the same membership limit to the users query. diff --git a/packages/better-auth/src/plugins/organization/adapter.ts b/packages/better-auth/src/plugins/organization/adapter.ts index 48a6226ab9..113bf91258 100644 --- a/packages/better-auth/src/plugins/organization/adapter.ts +++ b/packages/better-auth/src/plugins/organization/adapter.ts @@ -219,6 +219,8 @@ export const getOrgAdapter = ( ], }), ]); + // Prisma/Drizzle default findMany to ~100 when limit is omitted. + // Bound by the members result so every joined user row is fetched. const users = await adapter.findMany({ model: "user", where: [ @@ -228,6 +230,7 @@ export const getOrgAdapter = ( operator: "in", }, ], + limit: members[0].length, }); return { members: members[0].map((member) => { diff --git a/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts b/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts index 7bd80b9f93..c52938199a 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-members.test.ts @@ -288,6 +288,88 @@ describe("listMembers", async () => { }); }); +/** + * @see https://github.com/better-auth/better-auth/issues/9407 + */ +describe("listMembers with >100 members", async () => { + const { auth, signInWithTestUser } = await getTestInstance({ + plugins: [ + organization({ + membershipLimit: 500, + }), + ], + }); + const ctx = await auth.$context; + const { headers } = await signInWithTestUser(); + const client = createAuthClient({ + plugins: [organizationClient()], + baseURL: "http://localhost:3000/api/auth", + fetchOptions: { + customFetchImpl: async (url, init) => { + return auth.handler(new Request(url, init)); + }, + }, + }); + + const org = await client.organization.create({ + name: "large-org", + slug: "large-org", + fetchOptions: { + headers, + }, + }); + + // Owner + 110 members = 111 total (>100 default adapter limit) + for (let i = 0; i < 110; i++) { + const user = await ctx.adapter.create({ + model: "user", + data: { + email: `large-org-${i}@test.com`, + name: `large-org-${i}`, + }, + }); + await auth.api.addMember({ + body: { + organizationId: org.data?.id as string, + userId: user.id, + role: "member", + }, + }); + } + + // Prisma/Drizzle default findMany to ~100 rows when limit is omitted. + // Apply that default after seeding so setup is unaffected, then assert + // listMembers still returns every member user. + const originalFindMany = ctx.adapter.findMany.bind(ctx.adapter); + ctx.adapter.findMany = async (data) => { + return originalFindMany({ + ...data, + limit: data.limit ?? 100, + }); + }; + + it("should list all members when membershipLimit is above the adapter default", async () => { + const members = await client.organization.listMembers({ + fetchOptions: { + headers, + }, + query: { + organizationId: org.data?.id as string, + }, + }); + expect(members.error).toBeNull(); + expect(members.data?.members.length).toBe(111); + expect(members.data?.total).toBe(111); + expect( + members.data?.members.every( + (member) => + typeof member.user?.id === "string" && + typeof member.user?.email === "string", + ), + ).toBe(true); + }); +}); + describe("updateMemberRole", async () => { const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({ plugins: [organization()],