-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Prisma adapter uses db[model].update with non‑unique where (AND …) → PrismaClientValidationError #5929
Copy link
Copy link
Closed
Labels
lockedLocked conversations after being closed for 7 daysLocked conversations after being closed for 7 days
Description
Is this suited for github?
- Yes, this is suited for github
To Reproduce
-
Configure backend with Prisma & organizations:
import { prismaAdapter } from "better-auth/adapters/prisma"; import { organization } from "better-auth/plugins"; export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql" }), plugins: [ organization({ dynamicAccessControl: { enabled: true }, }), ], });
-
In the frontend, create a dynamic role, then update it (e.g., change permissions).
-
The backend responds with 500 Internal Server Error. Logs show the adapter performing
.update()with a non‑uniquewherefilter.
2025-11-12T11:19:49.689Z ERROR [Better Auth]: PrismaClientValidationError PrismaClientValidationError:
Invalid `db[model].update()` invocation in
…/node_modules/better-auth/dist/adapters/prisma-adapter/index.cjs:145:32
142 );
143 }
144 const whereClause = convertWhereClause(model, where);
→ 145 return await db[model].update({
where: {
AND: [
{ organizationId: "69uq9NtW3a4gDMOgY7LlKiE6phXBGsPr" },
{ id: "6VG0KRLviTxD1JJYOdN6k8oIBTRilfOz" }
],
? id?: String,
? OR?: OrganizationRoleWhereInput[],
? NOT?: OrganizationRoleWhereInput | OrganizationRoleWhereInput[],
? organizationId?: StringFilter | String,
? role?: StringFilter | String,
? permission?: StringFilter | String,
? createdAt?: DateTimeFilter | DateTime,
? updatedAt?: DateTimeNullableFilter | DateTime | Null,
? organization?: OrganizationScalarRelationFilter | OrganizationWhereInput
},
data: {
permission: "{\"organization\":[\"update\"],\"member\":[\"delete\"],\"ac\":[\"create\",\"update\"],\"team\":[\"update\",\"create\",\"delete\"],\"invitation\":[\"create\",\"cancel\"]}",
}
})
Argument `where` of type OrganizationRoleWhereUniqueInput needs at least one of `id` arguments.
at throwValidationException (.../node_modules/@prisma/client/src/runtime/core/errorRendering/throwValidationException.ts:45:9)
at ei.handleRequestError (.../node_modules/@prisma/client/src/runtime/RequestHandler.ts:202:7)
at ei.handleAndLogRequestError (.../node_modules/@prisma/client/src/runtime/RequestHandler.ts:174:12)
at ei.request (.../node_modules/@prisma/client/src/runtime/RequestHandler.ts:143:12)
at async a (.../node_modules/@prisma/client/src/runtime/getPrismaClient.ts:833:24)
at async Object.update (.../node_modules/better-auth/dist/adapters/prisma-adapter/index.cjs:145:16)
at async Object.update (.../node_modules/better-auth/dist/shared/better-auth.ucn9QAOT.cjs:498:19)
at async ...
Current vs. Expected behavior
Actual Behavior
The adapter issues the following call (excerpt):
return await db[model].update({
where: {
AND: [
{ organizationId: "69uq9NtW3a4gDMOgY7LlKiE6phXBGsPr" },
{ id: "6VG0KRLviTxD1JJYOdN6k8oIBTRilfOz" },
],
},
data,
});Prisma throws:
PrismaClientValidationError: Argument `where` of type OrganizationRoleWhereUniqueInput needs at least one of `id` arguments.
update expects a WhereUniqueInput (e.g., { id: "…" }). Passing AND turns it into a non‑unique WhereInput, which is only valid for updateMany.
Expected Behavior
Updating a dynamic organization role should succeed. The adapter should:
- Use
updateonly with a unique selector (e.g.,where: { id }), or - Fall back to
updateManywhen the providedwhereis non‑unique / compound.
return await db[model].update({
where: {
id: "6VG0KRLviTxD1JJYOdN6k8oIBTRilfOz"
organizationId: "69uq9NtW3a4gDMOgY7LlKiE6phXBGsPr" },
},
data,
});What version of Better Auth are you using?
1.3.34, 1.4.0-beta.20
System info
{
"system": {
"platform": "darwin",
"arch": "x64",
"version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:05 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_X86_64",
"release": "24.6.0",
"cpuCount": 12,
"cpuModel": "Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz",
"totalMemory": "32.00 GB",
"freeMemory": "0.78 GB"
},
"node": {
"version": "v20.19.5",
"env": "development"
},
"packageManager": {
"name": "npm",
"version": "11.6.2"
},
"frameworks": [
{
"name": "fastify",
"version": "^5.6.1"
}
],
"databases": [
{
"name": "@prisma/client",
"version": "^6.18.0"
}
],
"betterAuth": {
"version": "^1.3.34",
"config": {
"baseURL": "http://localhost:8080",
"trustedOrigins": [
"http://localhost:3000",
"http://localhost:8080",
"https://example.com",
"https://appleid.apple.com"
],
"session": {
"cookieCache": {
"enabled": true
},
"cookie": {
"sameSite": "lax",
"secure": false
}
},
"user": {
"deleteUser": {
"enabled": true
},
"create": {},
"update": {},
"additionalFields": {
"firstName": {
"type": "string",
"required": true
},
"lastName": {
"type": "string",
"required": true
},
"currentOrganizationId": {
"type": "string",
"required": false,
"input": true
}
},
"changeEmail": {
"enabled": true
}
},
"plugins": [
{
"name": "bearer",
"config": {
"id": "bearer",
"hooks": {
"before": [
{}
],
"after": [
{}
]
}
}
},
{
"name": "two-factor",
"config": {
"id": "two-factor",
"endpoints": {},
"hooks": {
"after": [
{}
]
},
"schema": {
"user": {
"fields": {
"twoFactorEnabled": {
"type": "boolean",
"required": false,
"defaultValue": false,
"input": false
}
}
},
"twoFactor": {
"fields": {
"secret": {
"type": "string",
"required": true,
"returned": false
},
"backupCodes": {
"type": "string",
"required": true,
"returned": false
},
"userId": {
"type": "string",
"required": true,
"returned": false,
"references": {
"model": "user",
"field": "id"
}
}
}
}
},
"rateLimit": [
{
"window": 10,
"max": 3
}
],
"$ERROR_CODES": {
"OTP_NOT_ENABLED": "OTP not enabled",
"OTP_HAS_EXPIRED": "OTP has expired",
"TOTP_NOT_ENABLED": "TOTP not enabled",
"TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled",
"BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled",
"INVALID_BACKUP_CODE": "Invalid backup code",
"INVALID_CODE": "Invalid code",
"TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.",
"INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie"
}
}
},
{
"name": "open-api",
"config": {
"id": "open-api",
"endpoints": {}
}
},
{
"name": "organization",
"config": {
"id": "organization",
"endpoints": {},
"schema": {
"organizationRole": {
"fields": {
"organizationId": {
"type": "string",
"required": true,
"references": {
"model": "organization",
"field": "id"
}
},
"role": {
"type": "string",
"required": true
},
"permission": {
"type": "string",
"required": true
},
"createdAt": {
"type": "date",
"required": true
},
"updatedAt": {
"type": "date",
"required": false
}
}
},
"organization": {
"fields": {
"name": {
"type": "string",
"required": true,
"sortable": true
},
"slug": {
"type": "string",
"required": true,
"unique": true,
"sortable": true
},
"logo": {
"type": "string",
"required": false
},
"createdAt": {
"type": "date",
"required": true
},
"metadata": {
"type": "string",
"required": false
},
"stripeId": {
"type": "string",
"required": false
},
"business": {
"type": "boolean",
"required": false,
"input": false
},
"deleted": {
"type": "boolean",
"required": false,
"input": false
}
}
},
"member": {
"fields": {
"organizationId": {
"type": "string",
"required": true,
"references": {
"model": "organization",
"field": "id"
}
},
"userId": {
"type": "string",
"required": true,
"references": {
"model": "user",
"field": "id"
}
},
"role": {
"type": "string",
"required": true,
"sortable": true,
"defaultValue": "member"
},
"createdAt": {
"type": "date",
"required": true
}
}
},
"invitation": {
"fields": {
"organizationId": {
"type": "string",
"required": true,
"references": {
"model": "organization",
"field": "id"
}
},
"email": {
"type": "string",
"required": true,
"sortable": true
},
"role": {
"type": "string",
"required": false,
"sortable": true
},
"status": {
"type": "string",
"required": true,
"sortable": true,
"defaultValue": "pending"
},
"expiresAt": {
"type": "date",
"required": true
},
"inviterId": {
"type": "string",
"references": {
"model": "user",
"field": "id"
},
"required": true
}
}
},
"session": {
"fields": {
"activeOrganizationId": {
"type": "string",
"required": false
}
}
}
},
"$Infer": {
"Organization": {},
"Invitation": {},
"Member": {},
"Team": {},
"TeamMember": {},
"ActiveOrganization": {}
},
"$ERROR_CODES": {
"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization",
"YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations",
"ORGANIZATION_ALREADY_EXISTS": "Organization already exists",
"ORGANIZATION_NOT_FOUND": "Organization not found",
"USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization",
"YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization",
"YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization",
"NO_ACTIVE_ORGANIZATION": "No active organization",
"USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization",
"MEMBER_NOT_FOUND": "Member not found",
"ROLE_NOT_FOUND": "Role not found",
"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team",
"TEAM_ALREADY_EXISTS": "Team already exists",
"TEAM_NOT_FOUND": "Team not found",
"YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner",
"YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner",
"YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member",
"YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization",
"USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization",
"INVITATION_NOT_FOUND": "Invitation not found",
"YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation",
"EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation",
"YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation",
"INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization",
"YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role",
"FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation",
"YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams",
"UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team",
"YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member",
"ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached",
"YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization",
"YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization",
"YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team",
"YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team",
"INVITATION_LIMIT_REACHED": "Invitation limit reached",
"TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached",
"USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team",
"YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team",
"YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team",
"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member",
"YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member",
"YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner",
"YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization",
"MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information",
"YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role",
"YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role",
"YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role",
"YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role",
"YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role",
"YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role",
"YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role",
"TOO_MANY_ROLES": "This organization has too many roles",
"INVALID_RESOURCE": "The provided permission includes an invalid resource",
"ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken",
"CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role"
},
"options": {
"ac": {
"statements": {
"organization": [
"update",
"delete"
],
"member": [
"create",
"update",
"delete"
],
"invitation": [
"create",
"cancel"
],
"team": [
"create",
"update",
"delete"
],
"ac": [
"create",
"read",
"update",
"delete"
]
}
},
"creatorRole": "owner",
"roles": {
"owner": {
"statements": {
"organization": [
"update",
"delete"
],
"member": [
"create",
"update",
"delete"
],
"invitation": [
"create",
"cancel"
],
"team": [
"create",
"update",
"delete"
],
"ac": [
"create",
"read",
"update",
"delete"
]
}
},
"admin": {
"statements": {
"organization": [
"update"
],
"invitation": [
"create",
"cancel"
],
"member": [
"create",
"update",
"delete"
],
"team": [
"create",
"update",
"delete"
],
"ac": [
"create",
"read",
"update",
"delete"
]
}
},
"member": {
"statements": {
"organization": [],
"member": [],
"invitation": [],
"team": [],
"ac": [
"read"
]
}
}
},
"dynamicAccessControl": {
"enabled": true
},
"schema": {
"organization": {
"create": {},
"update": {},
"additionalFields": {
"stripeId": {
"type": "string",
"required": false
},
"business": {
"type": "boolean",
"required": false,
"input": false
},
"deleted": {
"type": "boolean",
"required": false,
"input": false
}
}
}
}
}
}
}
],
"emailAndPassword": {
"enabled": true,
"requireEmailVerification": true
},
"emailVerification": {},
"socialProviders": {
"google": {
"clientId": "[REDACTED]",
"clientSecret": "[REDACTED]"
},
"apple": {},
"facebook": {}
}
}
}
}Which area(s) are affected? (Select all that apply)
Backend
Auth config (if applicable)
import { betterAuth } from "better-auth"
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_BACKEND_URL || 'http://localhost:8080',
// CORS Configuration
trustedOrigins: [
process.env.BETTER_AUTH_FRONTEND_URL || 'http://localhost:3000', // Frontend
process.env.BETTER_AUTH_BACKEND_URL || 'http://localhost:8080', // Backend
'https://example.com', // USED FOR TESTING / POSTMAN
'https://appleid.apple.com', // NEEDED FOR APPLE AUTH
],
// Session-Konfiguration (optional)
session: {
cookieCache: {
enabled: true,
},
cookie: {
sameSite: 'lax', // Important for OAuth flows
secure: false, // Set to true in production with HTTPS
domain: undefined, // Don't set a domain for localhost
},
},
// Database Configuration
database: prismaAdapter(prisma, { provider: 'postgresql' }),
user: {
deleteUser: {
enabled: true,
},
create: {
before: async (user, ctx) => {
return {
data: {
...user,
name: user.name ?? `${user.firstName} ${user.lastName}`,
},
};
},
},
update: {
before: async (user, ctx) => {
if (user.currentOrganizationId) {
const userId = ctx.context.session?.userId;
if (!userId)
throw new Error(
'User must be authenticated to update currentOrganizationId',
);
await validateUserIsMemberOfOrganization(
userId,
user.currentOrganizationId,
);
}
return { data: user };
},
},
additionalFields: {
firstName: {
type: 'string',
required: true,
},
lastName: {
type: 'string',
required: true,
},
currentOrganizationId: {
type: 'string',
required: false,
input: true,
},
},
changeEmail: {
enabled: true,
sendChangeEmailVerification: async ({ user, url, token }, request) => {
await sendEmailVerificationLink(user.email, url);
},
},
},
// Plugin Configuration
plugins: [
bearer(),
twoFactor(),
openAPI(),
organization({
ac: accessControl,
creatorRole: 'owner',
roles: { owner, admin, member },
dynamicAccessControl: {
enabled: true,
},
schema: {
organization: {
create: {
before: async (organization, ctx) => {
return {
data: organization,
};
},
},
update: {
before: async (organization, ctx) => {
return {
data: organization,
};
},
},
additionalFields: {
stripeId: {
type: 'string',
required: false,
},
business: {
type: 'boolean',
required: false,
input: false,
},
deleted: {
type: 'boolean',
required: false,
input: false,
},
},
},
},
async sendInvitationEmail(data) {
const inviteLink = `${process.env.BETTER_AUTH_FRONTEND_URL}/accept-invitation?invitationId=${data.id}`;
await sendOrganizationInvitationEmail({
email: data.email,
invitedByUsername: data.inviter.user.name,
invitedByEmail: data.inviter.user.email,
teamName: data.organization.name,
inviteLink,
});
},
}),
],
// Email and Password Configuration
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url, token }, request) => {
await sendEmailVerificationLink(user.email, url);
},
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
await sendEmailVerificationLink(user.email, url);
},
},
// Social Providers Configuration
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// redirectURI: 'http://localhost:8080/api/auth/callback/google',
},
apple: {
clientId: process.env.APPLE_CLIENT_ID!,
clientSecret: process.env.APPLE_CLIENT_SECRET!,
appBundleIdentifier: process.env.APPLE_APP_BUNDLE_IDENTIFIER!,
},
facebook: {
clientId: process.env.FACEBOOK_CLIENT_ID!,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
},
},
});Additional context
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
lockedLocked conversations after being closed for 7 daysLocked conversations after being closed for 7 days