[GH-ISSUE #2474] Prisma invalid invocation when trying to find/update/delete one entry without a unique property #9210

Closed
opened 2026-04-13 04:36:25 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @DevDuki on GitHub (Apr 29, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2474

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Setup better auth with prisma and the twoFactor() plugin
  2. Generate the schema with npx @better-auth/cli generate
  3. Migrate the new schema with prisma migrate dev
  4. Generate the new prisma client with prisma generate
  5. Enable 2FA on a user via BA
  6. Disable 2FA on a user via BA
  7. Check logs on the server

Current vs. Expected behavior

When BA tries to delete a twoFactor entity from the twoFactor table, it tries to access it via the userId property. For some reason prisma does not like it and throws this error:

prisma:error 
Invalid `prisma.twoFactor.delete()` invocation:

{
  where: {
    userId: "as34ViBwpxNeAgDdZMj4OnvkuPEWkwtT",
?   id?: String,
?   AND?: TwoFactorWhereInput | TwoFactorWhereInput[],
?   OR?: TwoFactorWhereInput[],
?   NOT?: TwoFactorWhereInput | TwoFactorWhereInput[],
?   secret?: StringFilter | String,
?   backupCodes?: StringFilter | String,
?   user?: UserScalarRelationFilter | UserWhereInput
  }
}

Argument `where` of type TwoFactorWhereUniqueInput needs at least one of `id` arguments. Available options are marked with ?.

I am a bit confused, because according to the generated schema a user can have multiple twoFactors, which is probably why prisma is complaining here. Perhaps BA has to use the deleteMany() function when calling prisma, instead of just delete().

What version of Better Auth are you using?

1.2.7

Provide environment information

- OS: MacOS Sequoia 15.1
- Browser: Arc

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

Backend

Auth config (if applicable)

export const auth = betterAuth({
	// Used as issuer for 2FA
	appName: 'Blah',

	// Session
	session: {
		expiresIn: SESSION_EXPIRES_IN
	},

	// Database
	database: prismaAdapter(prisma, {
		provider: 'sqlite'
	}),

	// Cookies
	advanced: {
		cookiePrefix: 'recipe'
	},

	// Email/Password
	emailAndPassword: {
		enabled: true,
		sendResetPassword: async ({ user, url }, request) => {
			const lng = getLanguageFromRequestHeaders(request);
			await EmailService.sendEmailRequest('RESET_PASSWORD', lng, {
				to: user.email,
				href: encodeURIComponent(url)
			});
		}
	},
	emailVerification: {
		sendOnSignUp: true,
		autoSignInAfterVerification: true,
		sendVerificationEmail: async ({ user, url }, request) => {
			const lng = getLanguageFromRequestHeaders(request);

			const newUrl = new URL(url);
			newUrl.searchParams.set('callbackURL', route('/(protected)'));

			await EmailService.sendEmailRequest('VERIFY_EMAIL', lng, {
				to: user.email,
				href: encodeURIComponent(newUrl.toString())
			});
		}
	},

	// User
	user: {
		additionalFields: {
			hasLifetimeAccess: {
				type: "boolean",
				required: false,
				defaultValue: false,
				input: true,
			}
		},
		changeEmail: {
			enabled: true,
			sendChangeEmailVerification: async ({ user, url }, request) => {
				const lng = getLanguageFromRequestHeaders(request);

				const newUrl = new URL(url);
				newUrl.searchParams.set('callbackURL', route('/(protected)'));

				await EmailService.sendEmailRequest('CONFIRM_CHANGE_EMAIL_REQUEST', lng, {
					to: user.email,
					href: encodeURIComponent(newUrl.toString())
				});
			}
		},
		deleteUser: {
			enabled: true,
			sendDeleteAccountVerification: async ({ user, url }, request) => {
				const lng = getLanguageFromRequestHeaders(request);

				const newUrl = new URL(url);
				newUrl.searchParams.set('callbackURL', route('/(protected)'));

				await EmailService.sendEmailRequest('CONFIRM_DELETE_ACCOUNT_REQUEST', lng, {
					to: user.email,
					href: encodeURIComponent(newUrl.toString())
				});
			}
		}
	},

	// Socials
	socialProviders: {
		google: {
			clientId: GOOGLE_CLIENT_ID,
			clientSecret: GOOGLE_CLIENT_SECRET
		},
		github: {
			clientId: GITHUB_CLIENT_ID,
			clientSecret: GITHUB_CLIENT_SECRET
		}
	},

	// Plugins
	plugins: [
		twoFactor(),
		passkey(),
		stripePlugin
	],

	// Redis
	secondaryStorage: {
		get: async (key) => {
			const value = await redis.get(REDIS_PREFIX + key);
			return value ? value : null;
		},
		set: async (key, value, ttl) => {
			if (ttl) await redis.set(REDIS_PREFIX + key, value, { EX: ttl });
			else await redis.set(REDIS_PREFIX + key, value);
		},
		delete: async (key) => {
			await redis.del(REDIS_PREFIX + key);
		}
	},

	// Hooks
	hooks: {
		before: createAuthMiddleware(async (ctx) => {
			if (ctx.path === '/get-session') {
				const sessionToken = await ctx.getSignedCookie(COOKIES.SESSION, BETTER_AUTH_SECRET);

				if (!sessionToken) {
					return;
				}

				await refreshUserInSessionIfNecessary(sessionToken);
			}
		})
	}
});

Additional context

This error actually happens in quite a few places, where BA tries to access one single thing via a non-unique property. I sometimes saw it popup too when using the Stripe plugin, but don't remember anymore in which scenario.

Originally created by @DevDuki on GitHub (Apr 29, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2474 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Setup better auth with prisma and the `twoFactor()` plugin 2. Generate the schema with `npx @better-auth/cli generate` 3. Migrate the new schema with `prisma migrate dev` 4. Generate the new prisma client with `prisma generate` 5. Enable 2FA on a user via BA 6. Disable 2FA on a user via BA 7. Check logs on the server ### Current vs. Expected behavior When BA tries to delete a twoFactor entity from the `twoFactor` table, it tries to access it via the `userId` property. For some reason prisma does not like it and throws this error: ``` prisma:error Invalid `prisma.twoFactor.delete()` invocation: { where: { userId: "as34ViBwpxNeAgDdZMj4OnvkuPEWkwtT", ? id?: String, ? AND?: TwoFactorWhereInput | TwoFactorWhereInput[], ? OR?: TwoFactorWhereInput[], ? NOT?: TwoFactorWhereInput | TwoFactorWhereInput[], ? secret?: StringFilter | String, ? backupCodes?: StringFilter | String, ? user?: UserScalarRelationFilter | UserWhereInput } } Argument `where` of type TwoFactorWhereUniqueInput needs at least one of `id` arguments. Available options are marked with ?. ``` I am a bit confused, because according to the generated schema a user can have multiple twoFactors, which is probably why prisma is complaining here. Perhaps BA has to use the `deleteMany()` function when calling prisma, instead of just `delete()`. ### What version of Better Auth are you using? 1.2.7 ### Provide environment information ```bash - OS: MacOS Sequoia 15.1 - Browser: Arc ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ // Used as issuer for 2FA appName: 'Blah', // Session session: { expiresIn: SESSION_EXPIRES_IN }, // Database database: prismaAdapter(prisma, { provider: 'sqlite' }), // Cookies advanced: { cookiePrefix: 'recipe' }, // Email/Password emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url }, request) => { const lng = getLanguageFromRequestHeaders(request); await EmailService.sendEmailRequest('RESET_PASSWORD', lng, { to: user.email, href: encodeURIComponent(url) }); } }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }, request) => { const lng = getLanguageFromRequestHeaders(request); const newUrl = new URL(url); newUrl.searchParams.set('callbackURL', route('/(protected)')); await EmailService.sendEmailRequest('VERIFY_EMAIL', lng, { to: user.email, href: encodeURIComponent(newUrl.toString()) }); } }, // User user: { additionalFields: { hasLifetimeAccess: { type: "boolean", required: false, defaultValue: false, input: true, } }, changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, url }, request) => { const lng = getLanguageFromRequestHeaders(request); const newUrl = new URL(url); newUrl.searchParams.set('callbackURL', route('/(protected)')); await EmailService.sendEmailRequest('CONFIRM_CHANGE_EMAIL_REQUEST', lng, { to: user.email, href: encodeURIComponent(newUrl.toString()) }); } }, deleteUser: { enabled: true, sendDeleteAccountVerification: async ({ user, url }, request) => { const lng = getLanguageFromRequestHeaders(request); const newUrl = new URL(url); newUrl.searchParams.set('callbackURL', route('/(protected)')); await EmailService.sendEmailRequest('CONFIRM_DELETE_ACCOUNT_REQUEST', lng, { to: user.email, href: encodeURIComponent(newUrl.toString()) }); } } }, // Socials socialProviders: { google: { clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET }, github: { clientId: GITHUB_CLIENT_ID, clientSecret: GITHUB_CLIENT_SECRET } }, // Plugins plugins: [ twoFactor(), passkey(), stripePlugin ], // Redis secondaryStorage: { get: async (key) => { const value = await redis.get(REDIS_PREFIX + key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await redis.set(REDIS_PREFIX + key, value, { EX: ttl }); else await redis.set(REDIS_PREFIX + key, value); }, delete: async (key) => { await redis.del(REDIS_PREFIX + key); } }, // Hooks hooks: { before: createAuthMiddleware(async (ctx) => { if (ctx.path === '/get-session') { const sessionToken = await ctx.getSignedCookie(COOKIES.SESSION, BETTER_AUTH_SECRET); if (!sessionToken) { return; } await refreshUserInSessionIfNecessary(sessionToken); } }) } }); ``` ### Additional context This error actually happens in quite a few places, where BA tries to access one single thing via a non-unique property. I sometimes saw it popup too when using the Stripe plugin, but don't remember anymore in which scenario.
GiteaMirror added the locked label 2026-04-13 04:36:25 -05:00
Author
Owner

@Kinfe123 commented on GitHub (May 31, 2025):

have you make sure you have synced your change to your db ?

<!-- gh-comment-id:2925701925 --> @Kinfe123 commented on GitHub (May 31, 2025): have you make sure you have synced your change to your db ?
Author
Owner

@DevDuki commented on GitHub (Jun 1, 2025):

Yes everything is synced.

<!-- gh-comment-id:2926658110 --> @DevDuki commented on GitHub (Jun 1, 2025): Yes everything is synced.
Author
Owner

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

can you please send your auth config as well ?

<!-- gh-comment-id:2982673078 --> @Kinfe123 commented on GitHub (Jun 18, 2025): can you please send your auth config as well ?
Author
Owner

@DevDuki commented on GitHub (Jun 19, 2025):

@Kinfe123 Added it in the issue description

<!-- gh-comment-id:2987371273 --> @DevDuki commented on GitHub (Jun 19, 2025): @Kinfe123 Added it in the issue description
Author
Owner

@DevDuki commented on GitHub (Jul 24, 2025):

@Bekacru You maybe wanna reopen this issue, because I can still reproduce this bug in 1.3.3.

<!-- gh-comment-id:3113827323 --> @DevDuki commented on GitHub (Jul 24, 2025): @Bekacru You maybe wanna reopen this issue, because I can still reproduce this bug in `1.3.3`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9210