[GH-ISSUE #6695] Missing passKey plugin in the better-auth package. #10596

Closed
opened 2026-04-13 06:50:24 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @C-EO on GitHub (Dec 11, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6695

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Use the example from the better-auth website.

  1. I searched up the site.
  2. Opened site.
  3. Clicked Create Sign in Box.
  4. Customized my box options and toggled the Passkey option.
  5. Clicked Continue.
  6. Chose Next as my framework.
  7. Copied the provided template code as is.
  8. Started my dev server by running pnpm dev. (This issue is reproducible with npm as well).
  9. Visited http://localhost:3000 in my browser.

Current vs. Expected behavior

Following the steps from the previous action, I expected the site to run smoothly without any issues, but instead this happened:

## Error Type
Build Error

## Error Message
Export passkeyClient doesn't exist in target module

## Build Output
./src/lib/auth-client.ts:2:1
Export passkeyClient doesn't exist in target module
  1 | import { createAuthClient } from "better-auth/react";
> 2 | import { passkeyClient } from "better-auth/client/plugins";
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  3 |
  4 | export const authClient = createAuthClient({
  5 |     baseURL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",

The export passkeyClient was not found in module [project]/node_modules/.pnpm/better-auth@1.4.6_next@16.0_115607b4b05bbf3c0a32df986fff3556/node_modules/better-auth/dist/client/plugins/index.mjs [app-client] (ecmascript).
Did you mean to import apiKeyClient?
All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the requested export doesn't exist.

Import traces:
  Client Component Browser:
    ./src/lib/auth-client.ts [Client Component Browser]
    ./src/components/account-menu.tsx [Client Component Browser]
    ./src/components/header.tsx [Client Component Browser]
    ./src/components/header.tsx [Server Component]
    ./src/app/layout.tsx [Server Component]

  Client Component SSR:
    ./src/lib/auth-client.ts [Client Component SSR]
    ./src/components/account-menu.tsx [Client Component SSR]
    ./src/components/header.tsx [Client Component SSR]
    ./src/components/header.tsx [Server Component]
    ./src/app/layout.tsx [Server Component]

Next.js version: 16.0.7 (Turbopack)

I also checked the npmjs.com website just to make sure it wasn't me with the problem. I also updated the package to the latest version.

After searching through the bundled npm package for the plugin [passkey], I couldn't find it.

Attached is the bundled code from better-auth/dist/client/plugins/index.mjs:

import "../../url-B7VXiggp.mjs";
import { i as useAuthQuery } from "../../proxy-DNjQepc2.mjs";
import "../../parser-g6CH-tVp.mjs";
import "../../client-BJRbyWu7.mjs";
import "../../access-BCQibqkF.mjs";
import { a as userAc, t as adminAc$1 } from "../../access-DZRRE6Tq.mjs";
import { t as hasPermission } from "../../has-permission-BxveqtYZ.mjs";
import { a as memberAc, o as ownerAc, r as defaultRoles, t as adminAc } from "../../access-BktEfzR6.mjs";
import { n as hasPermissionFn } from "../../permission-BZUPzNK6.mjs";
import { t as twoFactorClient } from "../../client-7xkXfvW4.mjs";
import { atom } from "nanostores";

//#region src/plugins/additional-fields/client.ts
const inferAdditionalFields = (schema) => {
	return {
		id: "additional-fields-client",
		$InferServerPlugin: {}
	};
};

//#endregion
//#region src/plugins/admin/client.ts
const adminClient = (options) => {
	const roles = {
		admin: adminAc$1,
		user: userAc,
		...options?.roles
	};
	return {
		id: "admin-client",
		$InferServerPlugin: {},
		getActions: () => ({ admin: { checkRolePermission: (data) => {
			return hasPermission({
				role: data.role,
				options: {
					ac: options?.ac,
					roles
				},
				permissions: data.permissions ?? data.permission
			});
		} } }),
		pathMethods: {
			"/admin/list-users": "GET",
			"/admin/stop-impersonating": "POST"
		}
	};
};

//#endregion
//#region src/plugins/anonymous/client.ts
const anonymousClient = () => {
	return {
		id: "anonymous",
		$InferServerPlugin: {},
		pathMethods: { "/sign-in/anonymous": "POST" },
		atomListeners: [{
			matcher: (path) => path === "/sign-in/anonymous",
			signal: "$sessionSignal"
		}]
	};
};

//#endregion
//#region src/plugins/api-key/client.ts
const apiKeyClient = () => {
	return {
		id: "api-key",
		$InferServerPlugin: {},
		pathMethods: {
			"/api-key/create": "POST",
			"/api-key/delete": "POST",
			"/api-key/delete-all-expired-api-keys": "POST"
		}
	};
};

//#endregion
//#region src/plugins/custom-session/client.ts
const customSessionClient = () => {
	return InferServerPlugin();
};

//#endregion
//#region src/plugins/device-authorization/client.ts
const deviceAuthorizationClient = () => {
	return {
		id: "device-authorization",
		$InferServerPlugin: {},
		pathMethods: {
			"/device/code": "POST",
			"/device/token": "POST",
			"/device": "GET",
			"/device/approve": "POST",
			"/device/deny": "POST"
		}
	};
};

//#endregion
//#region src/plugins/email-otp/client.ts
const emailOTPClient = () => {
	return {
		id: "email-otp",
		$InferServerPlugin: {},
		atomListeners: [{
			matcher: (path) => path === "/email-otp/verify-email" || path === "/sign-in/email-otp",
			signal: "$sessionSignal"
		}]
	};
};

//#endregion
//#region src/plugins/generic-oauth/client.ts
const genericOAuthClient = () => {
	return {
		id: "generic-oauth-client",
		$InferServerPlugin: {}
	};
};

//#endregion
//#region src/plugins/jwt/client.ts
const jwtClient = (options) => {
	const jwksPath = options?.jwks?.jwksPath ?? "/jwks";
	return {
		id: "better-auth-client",
		$InferServerPlugin: {},
		pathMethods: { [jwksPath]: "GET" },
		getActions: ($fetch) => ({ jwks: async (fetchOptions) => {
			return await $fetch(jwksPath, {
				method: "GET",
				...fetchOptions
			});
		} })
	};
};

//#endregion
//#region src/plugins/last-login-method/client.ts
function getCookieValue(name) {
	if (typeof document === "undefined") return null;
	const cookie = document.cookie.split("; ").find((row) => row.startsWith(`${name}=`));
	return cookie ? cookie.split("=")[1] : null;
}
/**
* Client-side plugin to retrieve the last used login method
*/
const lastLoginMethodClient = (config = {}) => {
	const cookieName = config.cookieName || "better-auth.last_used_login_method";
	return {
		id: "last-login-method-client",
		getActions() {
			return {
				getLastUsedLoginMethod: () => {
					return getCookieValue(cookieName);
				},
				clearLastUsedLoginMethod: () => {
					if (typeof document !== "undefined") document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
				},
				isLastUsedLoginMethod: (method) => {
					return getCookieValue(cookieName) === method;
				}
			};
		}
	};
};

//#endregion
//#region src/plugins/magic-link/client.ts
const magicLinkClient = () => {
	return {
		id: "magic-link",
		$InferServerPlugin: {}
	};
};

//#endregion
//#region src/plugins/multi-session/client.ts
const multiSessionClient = (options) => {
	return {
		id: "multi-session",
		$InferServerPlugin: {},
		atomListeners: [{
			matcher(path) {
				return path === "/multi-session/set-active";
			},
			signal: "$sessionSignal"
		}]
	};
};

//#endregion
//#region src/plugins/oidc-provider/client.ts
const oidcClient = () => {
	return {
		id: "oidc-client",
		$InferServerPlugin: {}
	};
};

//#endregion
//#region src/plugins/one-tap/client.ts
let isRequestInProgress = null;
function isFedCMSupported() {
	return typeof window !== "undefined" && "IdentityCredential" in window;
}
const oneTapClient = (options) => {
	return {
		id: "one-tap",
		fetchPlugins: [{
			id: "fedcm-signout-handle",
			name: "FedCM Sign-Out Handler",
			hooks: { async onResponse(ctx) {
				if (!ctx.request.url.toString().includes("/sign-out")) return;
				if (options.promptOptions?.fedCM === false || !isFedCMSupported()) return;
				navigator.credentials.preventSilentAccess();
			} }
		}],
		getActions: ($fetch, _) => {
			return { oneTap: async (opts, fetchOptions) => {
				if (isRequestInProgress && !isRequestInProgress.signal.aborted) {
					console.warn("A Google One Tap request is already in progress. Please wait.");
					return;
				}
				if (typeof window === "undefined" || !window.document) {
					console.warn("Google One Tap is only available in browser environments");
					return;
				}
				async function callback(idToken) {
					await $fetch("/one-tap/callback", {
						method: "POST",
						body: { idToken },
						...opts?.fetchOptions,
						...fetchOptions
					});
					if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) window.location.href = opts?.callbackURL ?? "/";
				}
				const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
				const contextValue = context ?? options.context ?? "signin";
				const clients = {
					fedCM: async () => {
						try {
							const identityCredential = await navigator.credentials.get({
								identity: {
									context: contextValue,
									providers: [{
										configURL: "https://accounts.google.com/gsi/fedcm.json",
										clientId: options.clientId,
										nonce: opts?.nonce
									}]
								},
								mediation: autoSelect ? "optional" : "required",
								signal: isRequestInProgress?.signal
							});
							if (!identityCredential?.token) {
								opts?.onPromptNotification?.(void 0);
								return;
							}
							try {
								await callback(identityCredential.token);
								return;
							} catch (error) {
								console.error("Error during FedCM callback:", error);
								throw error;
							}
						} catch (error) {
							if (error?.code && (error.code === 19 || error.code === 20)) {
								opts?.onPromptNotification?.(void 0);
								return;
							}
							throw error;
						}
					},
					oneTap: () => {
						return new Promise((resolve, reject) => {
							let isResolved = false;
							const baseDelay = options.promptOptions?.baseDelay ?? 1e3;
							const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
							window.google?.accounts.id.initialize({
								client_id: options.clientId,
								callback: async (response) => {
									isResolved = true;
									try {
										await callback(response.credential);
										resolve();
									} catch (error) {
										console.error("Error during One Tap callback:", error);
										reject(error);
									}
								},
								auto_select: autoSelect,
								cancel_on_tap_outside: cancelOnTapOutside,
								context: contextValue,
								ux_mode: opts?.uxMode || "popup",
								nonce: opts?.nonce,
								itp_support: true,
								...options.additionalOptions
							});
							const handlePrompt = (attempt) => {
								if (isResolved) return;
								window.google?.accounts.id.prompt((notification) => {
									if (isResolved) return;
									if (notification.isDismissedMoment && notification.isDismissedMoment()) if (attempt < maxAttempts) {
										const delay = Math.pow(2, attempt) * baseDelay;
										setTimeout(() => handlePrompt(attempt + 1), delay);
									} else opts?.onPromptNotification?.(notification);
									else if (notification.isSkippedMoment && notification.isSkippedMoment()) if (attempt < maxAttempts) {
										const delay = Math.pow(2, attempt) * baseDelay;
										setTimeout(() => handlePrompt(attempt + 1), delay);
									} else opts?.onPromptNotification?.(notification);
								});
							};
							handlePrompt(0);
						});
					}
				};
				if (isRequestInProgress) isRequestInProgress?.abort();
				isRequestInProgress = new AbortController();
				try {
					const client = options.promptOptions?.fedCM === false || !isFedCMSupported() ? "oneTap" : "fedCM";
					if (client === "oneTap") await loadGoogleScript();
					await clients[client]();
				} catch (error) {
					console.error("Error during Google One Tap flow:", error);
					throw error;
				} finally {
					isRequestInProgress = null;
				}
			} };
		},
		getAtoms($fetch) {
			return {};
		}
	};
};
const loadGoogleScript = () => {
	return new Promise((resolve) => {
		if (window.googleScriptInitialized) {
			resolve();
			return;
		}
		const script = document.createElement("script");
		script.src = "https://accounts.google.com/gsi/client";
		script.async = true;
		script.defer = true;
		script.onload = () => {
			window.googleScriptInitialized = true;
			resolve();
		};
		document.head.appendChild(script);
	});
};

//#endregion
//#region src/plugins/one-time-token/client.ts
const oneTimeTokenClient = () => {
	return {
		id: "one-time-token",
		$InferServerPlugin: {}
	};
};

//#endregion
//#region src/plugins/organization/client.ts
/**
* Using the same `hasPermissionFn` function, but without the need for a `ctx` parameter or the `organizationId` parameter.
*/
const clientSideHasPermission = (input) => {
	return hasPermissionFn(input, input.options.roles || defaultRoles);
};
const organizationClient = (options) => {
	const $listOrg = atom(false);
	const $activeOrgSignal = atom(false);
	const $activeMemberSignal = atom(false);
	const $activeMemberRoleSignal = atom(false);
	const roles = {
		admin: adminAc,
		member: memberAc,
		owner: ownerAc,
		...options?.roles
	};
	return {
		id: "organization",
		$InferServerPlugin: {},
		getActions: ($fetch, _$store, co) => ({
			$Infer: {
				ActiveOrganization: {},
				Organization: {},
				Invitation: {},
				Member: {},
				Team: {}
			},
			organization: { checkRolePermission: (data) => {
				return clientSideHasPermission({
					role: data.role,
					options: {
						ac: options?.ac,
						roles
					},
					permissions: data.permissions ?? data.permission
				});
			} }
		}),
		getAtoms: ($fetch) => {
			const listOrganizations = useAuthQuery($listOrg, "/organization/list", $fetch, { method: "GET" });
			return {
				$listOrg,
				$activeOrgSignal,
				$activeMemberSignal,
				$activeMemberRoleSignal,
				activeOrganization: useAuthQuery([$activeOrgSignal], "/organization/get-full-organization", $fetch, () => ({ method: "GET" })),
				listOrganizations,
				activeMember: useAuthQuery([$activeMemberSignal], "/organization/get-active-member", $fetch, { method: "GET" }),
				activeMemberRole: useAuthQuery([$activeMemberRoleSignal], "/organization/get-active-member-role", $fetch, { method: "GET" })
			};
		},
		pathMethods: {
			"/organization/get-full-organization": "GET",
			"/organization/list-user-teams": "GET"
		},
		atomListeners: [
			{
				matcher(path) {
					return path === "/organization/create" || path === "/organization/delete" || path === "/organization/update";
				},
				signal: "$listOrg"
			},
			{
				matcher(path) {
					return path.startsWith("/organization");
				},
				signal: "$activeOrgSignal"
			},
			{
				matcher(path) {
					return path.startsWith("/organization/set-active");
				},
				signal: "$sessionSignal"
			},
			{
				matcher(path) {
					return path.includes("/organization/update-member-role");
				},
				signal: "$activeMemberSignal"
			},
			{
				matcher(path) {
					return path.includes("/organization/update-member-role");
				},
				signal: "$activeMemberRoleSignal"
			}
		]
	};
};
const inferOrgAdditionalFields = (schema) => {
	return {};
};

//#endregion
//#region src/plugins/phone-number/client.ts
const phoneNumberClient = () => {
	return {
		id: "phoneNumber",
		$InferServerPlugin: {},
		atomListeners: [{
			matcher(path) {
				return path === "/phone-number/update" || path === "/phone-number/verify" || path === "/sign-in/phone-number";
			},
			signal: "$sessionSignal"
		}]
	};
};

//#endregion
//#region src/plugins/siwe/client.ts
const siweClient = () => {
	return {
		id: "siwe",
		$InferServerPlugin: {}
	};
};

//#endregion
//#region src/plugins/username/client.ts
const usernameClient = () => {
	return {
		id: "username",
		$InferServerPlugin: {},
		atomListeners: [{
			matcher: (path) => path === "/sign-in/username",
			signal: "$sessionSignal"
		}]
	};
};

//#endregion
//#region src/client/plugins/infer-plugin.ts
const InferServerPlugin = () => {
	return {
		id: "infer-server-plugin",
		$InferServerPlugin: {}
	};
};

//#endregion
export { InferServerPlugin, adminClient, anonymousClient, apiKeyClient, clientSideHasPermission, customSessionClient, deviceAuthorizationClient, emailOTPClient, genericOAuthClient, inferAdditionalFields, inferOrgAdditionalFields, jwtClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, oidcClient, oneTapClient, oneTimeTokenClient, organizationClient, phoneNumberClient, siweClient, twoFactorClient, usernameClient };

What version of Better Auth are you using?

1.4.6

System info

The output was non-existent. I even tried to export it to a file in my cwd and still got an empty file.

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

Client

Auth config (if applicable)

import { betterAuth } from 'better-auth';
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "pg";

// Assuming we are using the Supabase connection string which is valid for PG driver
const pool = createPool({
    connectionString: process.env.DATABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL?.replace('https://', 'postgres://postgres:[PASSWORD]@') // This is likely incorrect for Supabase, usually it's a separate string. 
    // fallback to just checking if user has a connection string variable defined
});

export const auth = betterAuth({
    database: pool, // We need to configure this correctly.
    emailAndPassword: {
        enabled: true,
        async sendResetPassword(data, request) {
            // Send an email to the user with a link to reset their password
            console.log("Reset Password Link:", data.url);
        },
    },
    socialProviders: {
        discord: {
            clientId: process.env.DISCORD_CLIENT_ID!,
            clientSecret: process.env.DISCORD_CLIENT_SECRET!
        },
        google: {
            clientId: process.env.GOOGLE_CLIENT_ID!,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET!
        }
    },
    plugins: [
        passkey(),
    ]
});

Additional context

No response

Originally created by @C-EO on GitHub (Dec 11, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6695 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Use the example from the `better-auth` website. 1. I searched up the site. 2. Opened site. 3. Clicked `Create Sign in Box`. 4. Customized my box options and toggled the Passkey option. 5. Clicked `Continue`. 6. Chose `Next` as my framework. 7. Copied the provided template code as is. 8. Started my dev server by running `pnpm dev`. (This issue is reproducible with `npm` as well). 9. Visited `http://localhost:3000` in my browser. ### Current vs. Expected behavior Following the steps from the previous action, I expected the site to run smoothly without any issues, but instead this happened: ```error.console ## Error Type Build Error ## Error Message Export passkeyClient doesn't exist in target module ## Build Output ./src/lib/auth-client.ts:2:1 Export passkeyClient doesn't exist in target module 1 | import { createAuthClient } from "better-auth/react"; > 2 | import { passkeyClient } from "better-auth/client/plugins"; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 | 4 | export const authClient = createAuthClient({ 5 | baseURL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000", The export passkeyClient was not found in module [project]/node_modules/.pnpm/better-auth@1.4.6_next@16.0_115607b4b05bbf3c0a32df986fff3556/node_modules/better-auth/dist/client/plugins/index.mjs [app-client] (ecmascript). Did you mean to import apiKeyClient? All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the requested export doesn't exist. Import traces: Client Component Browser: ./src/lib/auth-client.ts [Client Component Browser] ./src/components/account-menu.tsx [Client Component Browser] ./src/components/header.tsx [Client Component Browser] ./src/components/header.tsx [Server Component] ./src/app/layout.tsx [Server Component] Client Component SSR: ./src/lib/auth-client.ts [Client Component SSR] ./src/components/account-menu.tsx [Client Component SSR] ./src/components/header.tsx [Client Component SSR] ./src/components/header.tsx [Server Component] ./src/app/layout.tsx [Server Component] Next.js version: 16.0.7 (Turbopack) ``` I also checked the `npmjs.com` website just to make sure it wasn't me with the problem. I also updated the package to the latest version. After searching through the bundled npm package for the plugin [passkey], I couldn't find it. Attached is the bundled code from `better-auth/dist/client/plugins/index.mjs`: ```mjs import "../../url-B7VXiggp.mjs"; import { i as useAuthQuery } from "../../proxy-DNjQepc2.mjs"; import "../../parser-g6CH-tVp.mjs"; import "../../client-BJRbyWu7.mjs"; import "../../access-BCQibqkF.mjs"; import { a as userAc, t as adminAc$1 } from "../../access-DZRRE6Tq.mjs"; import { t as hasPermission } from "../../has-permission-BxveqtYZ.mjs"; import { a as memberAc, o as ownerAc, r as defaultRoles, t as adminAc } from "../../access-BktEfzR6.mjs"; import { n as hasPermissionFn } from "../../permission-BZUPzNK6.mjs"; import { t as twoFactorClient } from "../../client-7xkXfvW4.mjs"; import { atom } from "nanostores"; //#region src/plugins/additional-fields/client.ts const inferAdditionalFields = (schema) => { return { id: "additional-fields-client", $InferServerPlugin: {} }; }; //#endregion //#region src/plugins/admin/client.ts const adminClient = (options) => { const roles = { admin: adminAc$1, user: userAc, ...options?.roles }; return { id: "admin-client", $InferServerPlugin: {}, getActions: () => ({ admin: { checkRolePermission: (data) => { return hasPermission({ role: data.role, options: { ac: options?.ac, roles }, permissions: data.permissions ?? data.permission }); } } }), pathMethods: { "/admin/list-users": "GET", "/admin/stop-impersonating": "POST" } }; }; //#endregion //#region src/plugins/anonymous/client.ts const anonymousClient = () => { return { id: "anonymous", $InferServerPlugin: {}, pathMethods: { "/sign-in/anonymous": "POST" }, atomListeners: [{ matcher: (path) => path === "/sign-in/anonymous", signal: "$sessionSignal" }] }; }; //#endregion //#region src/plugins/api-key/client.ts const apiKeyClient = () => { return { id: "api-key", $InferServerPlugin: {}, pathMethods: { "/api-key/create": "POST", "/api-key/delete": "POST", "/api-key/delete-all-expired-api-keys": "POST" } }; }; //#endregion //#region src/plugins/custom-session/client.ts const customSessionClient = () => { return InferServerPlugin(); }; //#endregion //#region src/plugins/device-authorization/client.ts const deviceAuthorizationClient = () => { return { id: "device-authorization", $InferServerPlugin: {}, pathMethods: { "/device/code": "POST", "/device/token": "POST", "/device": "GET", "/device/approve": "POST", "/device/deny": "POST" } }; }; //#endregion //#region src/plugins/email-otp/client.ts const emailOTPClient = () => { return { id: "email-otp", $InferServerPlugin: {}, atomListeners: [{ matcher: (path) => path === "/email-otp/verify-email" || path === "/sign-in/email-otp", signal: "$sessionSignal" }] }; }; //#endregion //#region src/plugins/generic-oauth/client.ts const genericOAuthClient = () => { return { id: "generic-oauth-client", $InferServerPlugin: {} }; }; //#endregion //#region src/plugins/jwt/client.ts const jwtClient = (options) => { const jwksPath = options?.jwks?.jwksPath ?? "/jwks"; return { id: "better-auth-client", $InferServerPlugin: {}, pathMethods: { [jwksPath]: "GET" }, getActions: ($fetch) => ({ jwks: async (fetchOptions) => { return await $fetch(jwksPath, { method: "GET", ...fetchOptions }); } }) }; }; //#endregion //#region src/plugins/last-login-method/client.ts function getCookieValue(name) { if (typeof document === "undefined") return null; const cookie = document.cookie.split("; ").find((row) => row.startsWith(`${name}=`)); return cookie ? cookie.split("=")[1] : null; } /** * Client-side plugin to retrieve the last used login method */ const lastLoginMethodClient = (config = {}) => { const cookieName = config.cookieName || "better-auth.last_used_login_method"; return { id: "last-login-method-client", getActions() { return { getLastUsedLoginMethod: () => { return getCookieValue(cookieName); }, clearLastUsedLoginMethod: () => { if (typeof document !== "undefined") document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; }, isLastUsedLoginMethod: (method) => { return getCookieValue(cookieName) === method; } }; } }; }; //#endregion //#region src/plugins/magic-link/client.ts const magicLinkClient = () => { return { id: "magic-link", $InferServerPlugin: {} }; }; //#endregion //#region src/plugins/multi-session/client.ts const multiSessionClient = (options) => { return { id: "multi-session", $InferServerPlugin: {}, atomListeners: [{ matcher(path) { return path === "/multi-session/set-active"; }, signal: "$sessionSignal" }] }; }; //#endregion //#region src/plugins/oidc-provider/client.ts const oidcClient = () => { return { id: "oidc-client", $InferServerPlugin: {} }; }; //#endregion //#region src/plugins/one-tap/client.ts let isRequestInProgress = null; function isFedCMSupported() { return typeof window !== "undefined" && "IdentityCredential" in window; } const oneTapClient = (options) => { return { id: "one-tap", fetchPlugins: [{ id: "fedcm-signout-handle", name: "FedCM Sign-Out Handler", hooks: { async onResponse(ctx) { if (!ctx.request.url.toString().includes("/sign-out")) return; if (options.promptOptions?.fedCM === false || !isFedCMSupported()) return; navigator.credentials.preventSilentAccess(); } } }], getActions: ($fetch, _) => { return { oneTap: async (opts, fetchOptions) => { if (isRequestInProgress && !isRequestInProgress.signal.aborted) { console.warn("A Google One Tap request is already in progress. Please wait."); return; } if (typeof window === "undefined" || !window.document) { console.warn("Google One Tap is only available in browser environments"); return; } async function callback(idToken) { await $fetch("/one-tap/callback", { method: "POST", body: { idToken }, ...opts?.fetchOptions, ...fetchOptions }); if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) window.location.href = opts?.callbackURL ?? "/"; } const { autoSelect, cancelOnTapOutside, context } = opts ?? {}; const contextValue = context ?? options.context ?? "signin"; const clients = { fedCM: async () => { try { const identityCredential = await navigator.credentials.get({ identity: { context: contextValue, providers: [{ configURL: "https://accounts.google.com/gsi/fedcm.json", clientId: options.clientId, nonce: opts?.nonce }] }, mediation: autoSelect ? "optional" : "required", signal: isRequestInProgress?.signal }); if (!identityCredential?.token) { opts?.onPromptNotification?.(void 0); return; } try { await callback(identityCredential.token); return; } catch (error) { console.error("Error during FedCM callback:", error); throw error; } } catch (error) { if (error?.code && (error.code === 19 || error.code === 20)) { opts?.onPromptNotification?.(void 0); return; } throw error; } }, oneTap: () => { return new Promise((resolve, reject) => { let isResolved = false; const baseDelay = options.promptOptions?.baseDelay ?? 1e3; const maxAttempts = options.promptOptions?.maxAttempts ?? 5; window.google?.accounts.id.initialize({ client_id: options.clientId, callback: async (response) => { isResolved = true; try { await callback(response.credential); resolve(); } catch (error) { console.error("Error during One Tap callback:", error); reject(error); } }, auto_select: autoSelect, cancel_on_tap_outside: cancelOnTapOutside, context: contextValue, ux_mode: opts?.uxMode || "popup", nonce: opts?.nonce, itp_support: true, ...options.additionalOptions }); const handlePrompt = (attempt) => { if (isResolved) return; window.google?.accounts.id.prompt((notification) => { if (isResolved) return; if (notification.isDismissedMoment && notification.isDismissedMoment()) if (attempt < maxAttempts) { const delay = Math.pow(2, attempt) * baseDelay; setTimeout(() => handlePrompt(attempt + 1), delay); } else opts?.onPromptNotification?.(notification); else if (notification.isSkippedMoment && notification.isSkippedMoment()) if (attempt < maxAttempts) { const delay = Math.pow(2, attempt) * baseDelay; setTimeout(() => handlePrompt(attempt + 1), delay); } else opts?.onPromptNotification?.(notification); }); }; handlePrompt(0); }); } }; if (isRequestInProgress) isRequestInProgress?.abort(); isRequestInProgress = new AbortController(); try { const client = options.promptOptions?.fedCM === false || !isFedCMSupported() ? "oneTap" : "fedCM"; if (client === "oneTap") await loadGoogleScript(); await clients[client](); } catch (error) { console.error("Error during Google One Tap flow:", error); throw error; } finally { isRequestInProgress = null; } } }; }, getAtoms($fetch) { return {}; } }; }; const loadGoogleScript = () => { return new Promise((resolve) => { if (window.googleScriptInitialized) { resolve(); return; } const script = document.createElement("script"); script.src = "https://accounts.google.com/gsi/client"; script.async = true; script.defer = true; script.onload = () => { window.googleScriptInitialized = true; resolve(); }; document.head.appendChild(script); }); }; //#endregion //#region src/plugins/one-time-token/client.ts const oneTimeTokenClient = () => { return { id: "one-time-token", $InferServerPlugin: {} }; }; //#endregion //#region src/plugins/organization/client.ts /** * Using the same `hasPermissionFn` function, but without the need for a `ctx` parameter or the `organizationId` parameter. */ const clientSideHasPermission = (input) => { return hasPermissionFn(input, input.options.roles || defaultRoles); }; const organizationClient = (options) => { const $listOrg = atom(false); const $activeOrgSignal = atom(false); const $activeMemberSignal = atom(false); const $activeMemberRoleSignal = atom(false); const roles = { admin: adminAc, member: memberAc, owner: ownerAc, ...options?.roles }; return { id: "organization", $InferServerPlugin: {}, getActions: ($fetch, _$store, co) => ({ $Infer: { ActiveOrganization: {}, Organization: {}, Invitation: {}, Member: {}, Team: {} }, organization: { checkRolePermission: (data) => { return clientSideHasPermission({ role: data.role, options: { ac: options?.ac, roles }, permissions: data.permissions ?? data.permission }); } } }), getAtoms: ($fetch) => { const listOrganizations = useAuthQuery($listOrg, "/organization/list", $fetch, { method: "GET" }); return { $listOrg, $activeOrgSignal, $activeMemberSignal, $activeMemberRoleSignal, activeOrganization: useAuthQuery([$activeOrgSignal], "/organization/get-full-organization", $fetch, () => ({ method: "GET" })), listOrganizations, activeMember: useAuthQuery([$activeMemberSignal], "/organization/get-active-member", $fetch, { method: "GET" }), activeMemberRole: useAuthQuery([$activeMemberRoleSignal], "/organization/get-active-member-role", $fetch, { method: "GET" }) }; }, pathMethods: { "/organization/get-full-organization": "GET", "/organization/list-user-teams": "GET" }, atomListeners: [ { matcher(path) { return path === "/organization/create" || path === "/organization/delete" || path === "/organization/update"; }, signal: "$listOrg" }, { matcher(path) { return path.startsWith("/organization"); }, signal: "$activeOrgSignal" }, { matcher(path) { return path.startsWith("/organization/set-active"); }, signal: "$sessionSignal" }, { matcher(path) { return path.includes("/organization/update-member-role"); }, signal: "$activeMemberSignal" }, { matcher(path) { return path.includes("/organization/update-member-role"); }, signal: "$activeMemberRoleSignal" } ] }; }; const inferOrgAdditionalFields = (schema) => { return {}; }; //#endregion //#region src/plugins/phone-number/client.ts const phoneNumberClient = () => { return { id: "phoneNumber", $InferServerPlugin: {}, atomListeners: [{ matcher(path) { return path === "/phone-number/update" || path === "/phone-number/verify" || path === "/sign-in/phone-number"; }, signal: "$sessionSignal" }] }; }; //#endregion //#region src/plugins/siwe/client.ts const siweClient = () => { return { id: "siwe", $InferServerPlugin: {} }; }; //#endregion //#region src/plugins/username/client.ts const usernameClient = () => { return { id: "username", $InferServerPlugin: {}, atomListeners: [{ matcher: (path) => path === "/sign-in/username", signal: "$sessionSignal" }] }; }; //#endregion //#region src/client/plugins/infer-plugin.ts const InferServerPlugin = () => { return { id: "infer-server-plugin", $InferServerPlugin: {} }; }; //#endregion export { InferServerPlugin, adminClient, anonymousClient, apiKeyClient, clientSideHasPermission, customSessionClient, deviceAuthorizationClient, emailOTPClient, genericOAuthClient, inferAdditionalFields, inferOrgAdditionalFields, jwtClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, oidcClient, oneTapClient, oneTimeTokenClient, organizationClient, phoneNumberClient, siweClient, twoFactorClient, usernameClient }; ``` ### What version of Better Auth are you using? 1.4.6 ### System info ```bash The output was non-existent. I even tried to export it to a file in my cwd and still got an empty file. ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from 'better-auth'; import { passkey } from "better-auth/plugins/passkey"; import { createPool } from "pg"; // Assuming we are using the Supabase connection string which is valid for PG driver const pool = createPool({ connectionString: process.env.DATABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL?.replace('https://', 'postgres://postgres:[PASSWORD]@') // This is likely incorrect for Supabase, usually it's a separate string. // fallback to just checking if user has a connection string variable defined }); export const auth = betterAuth({ database: pool, // We need to configure this correctly. emailAndPassword: { enabled: true, async sendResetPassword(data, request) { // Send an email to the user with a link to reset their password console.log("Reset Password Link:", data.url); }, }, socialProviders: { discord: { clientId: process.env.DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET! }, google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! } }, plugins: [ passkey(), ] }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 06:50:24 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Dec 11, 2025):

Hello there, thanks for opening this issue! Based on the error message and your reproduction steps, it appears that the passkey plugin isn’t being exported in the current bundled package (v1.4.6) as expected. This might be due to one of two reasons: either the plugin was inadvertently removed in the latest build or there’s a packaging/export issue in this release. I recommend double-checking the changelog and release notes to see if there have been any changes related to the passkey feature. Additionally, you could try removing your node_modules and reinstalling to ensure your package isn’t corrupted. Have you also confirmed that the documentation you followed is updated for version 1.4.6? Tag @better-auth-agent if you would like an answer from the Agent. Do you have any other questions or details you’d like to share about your setup?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3642965479 --> @better-auth-agent[bot] commented on GitHub (Dec 11, 2025): Hello there, thanks for opening this issue! Based on the error message and your reproduction steps, it appears that the passkey plugin isn’t being exported in the current bundled package (v1.4.6) as expected. This might be due to one of two reasons: either the plugin was inadvertently removed in the latest build or there’s a packaging/export issue in this release. I recommend double-checking the changelog and release notes to see if there have been any changes related to the passkey feature. Additionally, you could try removing your node_modules and reinstalling to ensure your package isn’t corrupted. Have you also confirmed that the documentation you followed is updated for version 1.4.6? Tag @better-auth-agent if you would like an answer from the Agent. Do you have any other questions or details you’d like to share about your setup? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@dosubot[bot] commented on GitHub (Dec 11, 2025):

Hi @C-EO! I'm Dosu and I’m helping the better-auth team.

The error happens because passkeyClient is not exported from better-auth/client/plugins. The Passkey plugin is distributed as a separate package and must be installed and imported from @better-auth/passkey/client instead.

To fix this, run:

npm install @better-auth/passkey

Then update your client code to:

import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "@better-auth/passkey/client";

export const authClient = createAuthClient({
  plugins: [
    passkeyClient(),
    // ...other plugins
  ]
});

The main better-auth package does not bundle the Passkey client plugin—this is intentional and documented. You’ll also need to run the database migration or schema generation for Passkey support:

npx @better-auth/cli migrate
# or
npx @better-auth/cli generate

See the official docs for Passkey setup and usage.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3642976072 --> @dosubot[bot] commented on GitHub (Dec 11, 2025): <!-- Greeting --> Hi @C-EO! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The error happens because `passkeyClient` is not exported from `better-auth/client/plugins`. The Passkey plugin is distributed as a separate package and must be installed and imported from `@better-auth/passkey/client` instead. To fix this, run: ```bash npm install @better-auth/passkey ``` Then update your client code to: ```typescript import { createAuthClient } from "better-auth/client"; import { passkeyClient } from "@better-auth/passkey/client"; export const authClient = createAuthClient({ plugins: [ passkeyClient(), // ...other plugins ] }); ``` The main `better-auth` package does not bundle the Passkey client plugin—this is intentional and documented. You’ll also need to run the database migration or schema generation for Passkey support: ```bash npx @better-auth/cli migrate # or npx @better-auth/cli generate ``` See the [official docs for Passkey setup and usage](https://github.com/better-auth/better-auth/blob/3cb5ded4c7eecfc57b47cceb7b43084d5e491ed1/docs/content/docs/plugins/passkey.mdx#L16-L403). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/37e0f559-cbfa-4fdb-8689-2d0323f4f689?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=The%20error%20happens%20because%20%60passkeyClient%60%20is%20not%20exported%20from%20%60better-auth/client/plugins%60.%20The%20Passkey%20plugin%20is%20distributed%20as%20a%20separate%20package%20and%20must%20be%20installed%20and%20imported%20from%20%60%40better-auth/passkey/client%60%20instead.%0A%0ATo%20fix%20this%2C%20run%3A%0A%0A%60%60%60bash%0Anpm%20install%20%40better-auth/passkey%0A%60%60%60%0A%0AThen%20update%20your%20client%20code%20to%3A%0A%0A%60%60%60typescript%0Aimport%20%7B%20createAuthClient%20%7D%20from%20%22better-auth/client%22%3B%0Aimport%20%7B%20passkeyClient%20%7D%20from%20%22%40better-auth/passkey/client%22%3B%0A%0Aexport%20const%20authClient%20%3D%20createAuthClient%28%7B%0A%20%20plugins%3A%20%5B%0A%20%20%20%20passkeyClient%28%29%2C%0A%20%20%20%20//%20...other%20plugins%0A%20%20%5D%0A%7D%29%3B%0A%60%60%60%0A%0AThe%20main%20%60better-auth%60%20package%20does%20not%20bundle%20the%20Passkey%20client%20plugin%E2%80%94this%20is%20intentional%20and%20documented.%20You%E2%80%99ll%20also%20need%20to%20run%20the%20database%20migration%20or%20schema%20generation%20for%20Passkey%20support%3A%0A%0A%60%60%60bash%0Anpx%20%40better-auth/cli%20migrate%0A%23%20or%0Anpx%20%40better-auth/cli%20generate%0A%60%60%60%0A%0ASee%20the%20%5Bofficial%20docs%20for%20Passkey%20setup%20and%20usage%5D%28https%3A//github.com/better-auth/better-auth/blob/3cb5ded4c7eecfc57b47cceb7b43084d5e491ed1/docs/content/docs/plugins/passkey.mdx%23L16-L403%29.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6695)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10596