[@better-auth/passkeys] TypeError: Reflect.getMetadata is not a function #2731

Closed
opened 2026-03-13 10:15:59 -05:00 by GiteaMirror · 16 comments
Owner

Originally created by @amruthpillai on GitHub (Jan 19, 2026).

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

This is the error stack trace:

2026-01-19 10:25:19.439 [error] TypeError: Reflect.getMetadata is not a function
    at getParamInfo (file:///var/task/_libs/@better-auth/passkey.mjs:6351:23)
    at file:///var/task/_libs/@better-auth/passkey.mjs:6820:24
    at __decorate (file:///var/task/_libs/@aws-crypto/crc32.mjs:23:92)
    at file:///var/task/_libs/@better-auth/passkey.mjs:7316:31
    at ModuleJob.run (node:internal/modules/esm/module_job:345:25)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
    at async d (/opt/rust/nodejs.js:17:24892)
Node.js process exited with exit status: 1. The logs above can help with debugging the issue.

On a simple node server, I am able to circumvent this by running node -r reflect-metadata ./path/to/script, but this does not carry over on serverless platforms like Netlify or Vercel.

I have repository that's making use of this package right now at https://github.com/amruthpillai/reactive-resume/

If you were to fork this repository and point it at the new repo, without the need for any environment variables, it should throw the error on the logs (runtime error, not build error).

Current vs. Expected behavior

On a successful build, it should display the landing page of the app.

What version of Better Auth are you using?

^1.5.0-beta.8 (but also happens on stable)

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:40 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6000",
    "release": "25.2.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Max",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.28 GB"
  },
  "node": {
    "version": "v24.12.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.7.0"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.2.3"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.17.1"
    },
    {
      "name": "drizzle",
      "version": "^1.0.0-beta.11-05230d9"
    }
  ],
  "betterAuth": {
    "version": "^1.5.0-beta.8",
    "config": null
  }
}

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

Package

Auth config (if applicable)

import { BetterAuthError } from "@better-auth/core/error";
import { passkey } from "@better-auth/passkey";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { apiKey, type GenericOAuthConfig, genericOAuth, twoFactor } from "better-auth/plugins";
import { username } from "better-auth/plugins/username";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
import { hashPassword, verifyPassword } from "@/utils/password";
import { generateId, toUsername } from "@/utils/string";
import { schema } from "../drizzle";
import { sendEmail } from "../email/service";

function isCustomOAuthProviderEnabled() {
	const hasDiscovery = Boolean(env.OAUTH_DISCOVERY_URL);
	const hasManual =
		Boolean(env.OAUTH_AUTHORIZATION_URL) && Boolean(env.OAUTH_TOKEN_URL) && Boolean(env.OAUTH_USER_INFO_URL);

	return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual);
}

const getAuthConfig = () => {
	const authConfigs: GenericOAuthConfig[] = [];

	if (isCustomOAuthProviderEnabled()) {
		authConfigs.push({
			providerId: "custom",
			clientId: env.OAUTH_CLIENT_ID as string,
			clientSecret: env.OAUTH_CLIENT_SECRET as string,
			discoveryUrl: env.OAUTH_DISCOVERY_URL,
			authorizationUrl: env.OAUTH_AUTHORIZATION_URL,
			tokenUrl: env.OAUTH_TOKEN_URL,
			userInfoUrl: env.OAUTH_USER_INFO_URL,
			scopes: env.OAUTH_SCOPES,
			redirectURI: `${env.APP_URL}/api/auth/oauth2/callback/custom`,
			mapProfileToUser: async (profile) => {
				if (!profile.email) {
					throw new BetterAuthError(
						"OAuth Provider did not return an email address. This is required for user creation.",
						{ cause: "EMAIL_REQUIRED" },
					);
				}

				const email = profile.email;
				const name = profile.name ?? profile.preferred_username ?? email.split("@")[0];
				const username = profile.preferred_username ?? email.split("@")[0];
				const image = profile.image ?? profile.picture ?? profile.avatar_url;

				return {
					name,
					email,
					image,
					username,
					displayUsername: username,
					emailVerified: true,
				};
			},
		} satisfies GenericOAuthConfig);
	}

	return betterAuth({
		appName: "Reactive Resume",

		baseURL: env.APP_URL,
		secret: env.AUTH_SECRET,

		database: drizzleAdapter(db, { schema, provider: "pg" }),

		telemetry: { enabled: false },
		trustedOrigins: [env.APP_URL],
		advanced: {
			database: { generateId },
			useSecureCookies: env.APP_URL.startsWith("https://"),
		},

		emailAndPassword: {
			enabled: true,
			autoSignIn: true,
			minPasswordLength: 6,
			maxPasswordLength: 64,
			requireEmailVerification: false,
			disableSignUp: env.FLAG_DISABLE_SIGNUP,
			sendResetPassword: async ({ user, url }) => {
				await sendEmail({
					to: user.email,
					subject: "Reset your password",
					text: `To reset your password, please visit the following URL: ${url}. If you did not request a password reset, please ignore this email.`,
				});
			},
			password: {
				hash: (password) => hashPassword(password),
				verify: ({ password, hash }) => verifyPassword(password, hash),
			},
		},

		emailVerification: {
			sendOnSignUp: true,
			autoSignInAfterVerification: true,
			sendVerificationEmail: async ({ user, url }) => {
				await sendEmail({
					to: user.email,
					subject: "Verify your email",
					text: `You recently signed up for an account on Reactive Resume.\nTo verify your email, please visit the following URL: ${url}`,
				});
			},
		},

		user: {
			changeEmail: {
				enabled: true,
				sendChangeEmailVerification: async ({ user, newEmail, url }) => {
					await sendEmail({
						to: newEmail,
						subject: "Verify your new email",
						text: `You recently requested to change your email on Reactive Resume from ${user.email} to ${newEmail}.\nTo verify this change, please visit the following URL: ${url}\nIf you did not request this change, please ignore this email.`,
					});
				},
			},
			additionalFields: {
				username: {
					type: "string",
					required: true,
				},
			},
		},

		account: {
			accountLinking: {
				enabled: true,
				trustedProviders: ["google", "github"],
			},
		},

		socialProviders: {
			google: {
				enabled: !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET,
				// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
				clientId: env.GOOGLE_CLIENT_ID!,
				// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
				clientSecret: env.GOOGLE_CLIENT_SECRET!,
				mapProfileToUser: async (profile) => {
					return {
						name: profile.name,
						email: profile.email,
						image: profile.picture,
						username: profile.email.split("@")[0],
						displayUsername: profile.email.split("@")[0],
						emailVerified: true,
					};
				},
			},

			github: {
				enabled: !!env.GITHUB_CLIENT_ID && !!env.GITHUB_CLIENT_SECRET,
				// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
				clientId: env.GITHUB_CLIENT_ID!,
				// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
				clientSecret: env.GITHUB_CLIENT_SECRET!,
				mapProfileToUser: async (profile) => {
					return {
						name: profile.name,
						email: profile.email,
						image: profile.avatar_url,
						username: profile.login,
						displayUsername: profile.login,
						emailVerified: true,
					};
				},
			},
		},

		plugins: [
			apiKey(),
			username({
				minUsernameLength: 3,
				maxUsernameLength: 64,
				usernameNormalization: (value) => toUsername(value),
				displayUsernameNormalization: (value) => toUsername(value),
				validationOrder: { username: "post-normalization", displayUsername: "post-normalization" },
			}),
			twoFactor({
				issuer: "Reactive Resume",
			}),
			passkey({
				rpName: "Reactive Resume",
				rpID: new URL(env.APP_URL).hostname,
				origin: env.APP_URL,
			}),
			genericOAuth({
				config: authConfigs,
			}),
			tanstackStartCookies(),
		],
	});
};

export const auth = getAuthConfig();

Additional context

No response

Originally created by @amruthpillai on GitHub (Jan 19, 2026). Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce This is the error stack trace: ``` 2026-01-19 10:25:19.439 [error] TypeError: Reflect.getMetadata is not a function at getParamInfo (file:///var/task/_libs/@better-auth/passkey.mjs:6351:23) at file:///var/task/_libs/@better-auth/passkey.mjs:6820:24 at __decorate (file:///var/task/_libs/@aws-crypto/crc32.mjs:23:92) at file:///var/task/_libs/@better-auth/passkey.mjs:7316:31 at ModuleJob.run (node:internal/modules/esm/module_job:345:25) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26) at async d (/opt/rust/nodejs.js:17:24892) Node.js process exited with exit status: 1. The logs above can help with debugging the issue. ``` On a simple node server, I am able to circumvent this by running `node -r reflect-metadata ./path/to/script`, but this does not carry over on serverless platforms like Netlify or Vercel. I have repository that's making use of this package right now at https://github.com/amruthpillai/reactive-resume/ If you were to fork this repository and point it at the new repo, without the need for any environment variables, it should throw the error on the logs (runtime error, not build error). ### Current vs. Expected behavior On a successful build, it should display the landing page of the app. ### What version of Better Auth are you using? ^1.5.0-beta.8 (but also happens on stable) ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:40 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6000", "release": "25.2.0", "cpuCount": 10, "cpuModel": "Apple M1 Max", "totalMemory": "32.00 GB", "freeMemory": "0.28 GB" }, "node": { "version": "v24.12.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.7.0" }, "frameworks": [ { "name": "react", "version": "^19.2.3" } ], "databases": [ { "name": "pg", "version": "^8.17.1" }, { "name": "drizzle", "version": "^1.0.0-beta.11-05230d9" } ], "betterAuth": { "version": "^1.5.0-beta.8", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript import { BetterAuthError } from "@better-auth/core/error"; import { passkey } from "@better-auth/passkey"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { betterAuth } from "better-auth/minimal"; import { apiKey, type GenericOAuthConfig, genericOAuth, twoFactor } from "better-auth/plugins"; import { username } from "better-auth/plugins/username"; import { tanstackStartCookies } from "better-auth/tanstack-start"; import { db } from "@/integrations/drizzle/client"; import { env } from "@/utils/env"; import { hashPassword, verifyPassword } from "@/utils/password"; import { generateId, toUsername } from "@/utils/string"; import { schema } from "../drizzle"; import { sendEmail } from "../email/service"; function isCustomOAuthProviderEnabled() { const hasDiscovery = Boolean(env.OAUTH_DISCOVERY_URL); const hasManual = Boolean(env.OAUTH_AUTHORIZATION_URL) && Boolean(env.OAUTH_TOKEN_URL) && Boolean(env.OAUTH_USER_INFO_URL); return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual); } const getAuthConfig = () => { const authConfigs: GenericOAuthConfig[] = []; if (isCustomOAuthProviderEnabled()) { authConfigs.push({ providerId: "custom", clientId: env.OAUTH_CLIENT_ID as string, clientSecret: env.OAUTH_CLIENT_SECRET as string, discoveryUrl: env.OAUTH_DISCOVERY_URL, authorizationUrl: env.OAUTH_AUTHORIZATION_URL, tokenUrl: env.OAUTH_TOKEN_URL, userInfoUrl: env.OAUTH_USER_INFO_URL, scopes: env.OAUTH_SCOPES, redirectURI: `${env.APP_URL}/api/auth/oauth2/callback/custom`, mapProfileToUser: async (profile) => { if (!profile.email) { throw new BetterAuthError( "OAuth Provider did not return an email address. This is required for user creation.", { cause: "EMAIL_REQUIRED" }, ); } const email = profile.email; const name = profile.name ?? profile.preferred_username ?? email.split("@")[0]; const username = profile.preferred_username ?? email.split("@")[0]; const image = profile.image ?? profile.picture ?? profile.avatar_url; return { name, email, image, username, displayUsername: username, emailVerified: true, }; }, } satisfies GenericOAuthConfig); } return betterAuth({ appName: "Reactive Resume", baseURL: env.APP_URL, secret: env.AUTH_SECRET, database: drizzleAdapter(db, { schema, provider: "pg" }), telemetry: { enabled: false }, trustedOrigins: [env.APP_URL], advanced: { database: { generateId }, useSecureCookies: env.APP_URL.startsWith("https://"), }, emailAndPassword: { enabled: true, autoSignIn: true, minPasswordLength: 6, maxPasswordLength: 64, requireEmailVerification: false, disableSignUp: env.FLAG_DISABLE_SIGNUP, sendResetPassword: async ({ user, url }) => { await sendEmail({ to: user.email, subject: "Reset your password", text: `To reset your password, please visit the following URL: ${url}. If you did not request a password reset, please ignore this email.`, }); }, password: { hash: (password) => hashPassword(password), verify: ({ password, hash }) => verifyPassword(password, hash), }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { await sendEmail({ to: user.email, subject: "Verify your email", text: `You recently signed up for an account on Reactive Resume.\nTo verify your email, please visit the following URL: ${url}`, }); }, }, user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, newEmail, url }) => { await sendEmail({ to: newEmail, subject: "Verify your new email", text: `You recently requested to change your email on Reactive Resume from ${user.email} to ${newEmail}.\nTo verify this change, please visit the following URL: ${url}\nIf you did not request this change, please ignore this email.`, }); }, }, additionalFields: { username: { type: "string", required: true, }, }, }, account: { accountLinking: { enabled: true, trustedProviders: ["google", "github"], }, }, socialProviders: { google: { enabled: !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET, // biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null clientId: env.GOOGLE_CLIENT_ID!, // biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null clientSecret: env.GOOGLE_CLIENT_SECRET!, mapProfileToUser: async (profile) => { return { name: profile.name, email: profile.email, image: profile.picture, username: profile.email.split("@")[0], displayUsername: profile.email.split("@")[0], emailVerified: true, }; }, }, github: { enabled: !!env.GITHUB_CLIENT_ID && !!env.GITHUB_CLIENT_SECRET, // biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null clientId: env.GITHUB_CLIENT_ID!, // biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null clientSecret: env.GITHUB_CLIENT_SECRET!, mapProfileToUser: async (profile) => { return { name: profile.name, email: profile.email, image: profile.avatar_url, username: profile.login, displayUsername: profile.login, emailVerified: true, }; }, }, }, plugins: [ apiKey(), username({ minUsernameLength: 3, maxUsernameLength: 64, usernameNormalization: (value) => toUsername(value), displayUsernameNormalization: (value) => toUsername(value), validationOrder: { username: "post-normalization", displayUsername: "post-normalization" }, }), twoFactor({ issuer: "Reactive Resume", }), passkey({ rpName: "Reactive Resume", rpID: new URL(env.APP_URL).hostname, origin: env.APP_URL, }), genericOAuth({ config: authConfigs, }), tanstackStartCookies(), ], }); }; export const auth = getAuthConfig(); ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 10:15:59 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Jan 19, 2026):

The error you're encountering, TypeError: Reflect.getMetadata is not a function, may be related to missing or misconfigured reflect-metadata in your project. Running node -r reflect-metadata works locally but may not be applicable on serverless platforms like Netlify or Vercel without additional configuration.

Here are a few suggestions to troubleshoot the issue:

  1. Ensure reflect-metadata is installed: Check that reflect-metadata is installed in your project by running npm install reflect-metadata.

  2. Import reflect-metadata: Make sure you're importing reflect-metadata at the top of your entry file (usually index.ts or app.ts), as follows:

    import "reflect-metadata";
    
  3. Check serverless configuration: Some serverless platforms have specific ways of handling dependencies and startup scripts. Review the documentation for both Vercel and Netlify to ensure any special requirements for handling packages at runtime are met.

  4. Debug logs: If the issue persists, consider adding logging around where this error is thrown to further diagnose what may be missing or misfiring in your application setup.

If you're still having issues after following these steps, feel free to tag @bekacru or @ping__ for further assistance.

What specific environment settings or steps have you tried so far?

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

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Jan 19, 2026): The error you're encountering, `TypeError: Reflect.getMetadata is not a function`, may be related to missing or misconfigured reflect-metadata in your project. Running `node -r reflect-metadata` works locally but may not be applicable on serverless platforms like Netlify or Vercel without additional configuration. Here are a few suggestions to troubleshoot the issue: 1. **Ensure reflect-metadata is installed**: Check that `reflect-metadata` is installed in your project by running `npm install reflect-metadata`. 2. **Import reflect-metadata**: Make sure you're importing `reflect-metadata` at the top of your entry file (usually `index.ts` or `app.ts`), as follows: ```typescript import "reflect-metadata"; ``` 3. **Check serverless configuration**: Some serverless platforms have specific ways of handling dependencies and startup scripts. Review the documentation for both Vercel and Netlify to ensure any special requirements for handling packages at runtime are met. 4. **Debug logs**: If the issue persists, consider adding logging around where this error is thrown to further diagnose what may be missing or misfiring in your application setup. If you're still having issues after following these steps, feel free to tag @bekacru or @ping__ for further assistance. What specific environment settings or steps have you tried so far? _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/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@amruthpillai commented on GitHub (Jan 19, 2026):

@ping__ @Bekacru I have tried the above solutions, even before creating the issue, but it still doesn't work. Would like for someone to take a look at this.

@amruthpillai commented on GitHub (Jan 19, 2026): @ping__ @Bekacru I have tried the above solutions, even before creating the issue, but it still doesn't work. Would like for someone to take a look at this.
Author
Owner

@bytaesu commented on GitHub (Jan 19, 2026):

Hi @amruthpillai, I'll look into this 🙂

@bytaesu commented on GitHub (Jan 19, 2026): Hi @amruthpillai, I'll look into this 🙂
Author
Owner

@himself65 commented on GitHub (Jan 20, 2026):

Hi, I think reflect-metadata is not from our side. Also, we don't even have the getParamInfo function in the codebase.

BTW, reflect-metadata has been deprecated for over 2 years. As a prev TC39 member, I would recommend you to use decorator stage 3

@himself65 commented on GitHub (Jan 20, 2026): Hi, I think reflect-metadata is not from our side. Also, we don't even have the `getParamInfo` function in the codebase. BTW, [reflect-metadata](https://github.com/microsoft/reflect-metadata) has been deprecated for over 2 years. As a prev TC39 member, I would recommend you to use [decorator stage 3](https://github.com/tc39/proposal-decorators)
Author
Owner

@CarlosZiegler commented on GitHub (Jan 21, 2026):

I have same Issue today after update my app :(

@CarlosZiegler commented on GitHub (Jan 21, 2026): I have same Issue today after update my app :(
Author
Owner

@amruthpillai commented on GitHub (Jan 21, 2026):

@himself65 I don't use reflect-metadata in my app either. I don't even use decorators. According to my error stack trace, it suggested that the error originated from the passkeys library, or one of it's dependencies. And it is true, because when I remove any passkeys related code, the app runs perfectly fine.

@amruthpillai commented on GitHub (Jan 21, 2026): @himself65 I don't use reflect-metadata in my app either. I don't even use decorators. According to my error stack trace, it suggested that the error originated from the passkeys library, or one of it's dependencies. And it is true, because when I remove any passkeys related code, the app runs perfectly fine.
Author
Owner

@CarlosZiegler commented on GitHub (Jan 21, 2026):

Hey I added a polifill and works as before:

import tailwindcss from "@tailwindcss/vite";
import { devtools } from "@tanstack/devtools-vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { config } from "dotenv";
import { nitro } from "nitro/vite";
import type { Plugin } from "vite";
import { defineConfig } from "vite";
import { postgres } from "vite-plugin-db";
import tsConfigPaths from "vite-tsconfig-paths";

config();

// Polyfill for Reflect.getMetadata (required by @better-auth/passkey)
const REFLECT_POLYFILL = `
if (typeof Reflect.getMetadata !== "function") {
  const m = new WeakMap();
  const get = (t, p) => m.get(t)?.get(p);
  const set = (t, p) => {
    let a = m.get(t); if (!a) { a = new Map(); m.set(t, a); }
    let b = a.get(p); if (!b) { b = new Map(); a.set(p, b); }
    return b;
  };
  const find = (k, t, p) => { const x = get(t, p); if (x?.has(k)) return x.get(k); const pr = Object.getPrototypeOf(t); return pr ? find(k, pr, p) : undefined; };
  Reflect.getMetadata = (k, t, p) => find(k, t, p);
  Reflect.getOwnMetadata = (k, t, p) => get(t, p)?.get(k);
  Reflect.defineMetadata = (k, v, t, p) => set(t, p).set(k, v);
  Reflect.hasMetadata = (k, t, p) => find(k, t, p) !== undefined;
  Reflect.hasOwnMetadata = (k, t, p) => get(t, p)?.has(k) ?? false;
  Reflect.metadata = (k, v) => (t, p) => set(t, p).set(k, v);
}
`;

function reflectPolyfillPlugin(): Plugin {
  return {
    name: "reflect-polyfill",
    renderChunk(code, chunk) {
      if (chunk.fileName.includes("passkey")) {
        return REFLECT_POLYFILL + code;
      }
      return null;
    },
  };
}

export default defineConfig({
  optimizeDeps: {
    entries: ["src/**/*.{js,jsx,ts,tsx}"],
    exclude: [
      "katex",
      "pdfjs",
      "pdf-parse",
      "qrcode.react",
      "react-to-print",
      "@hookform/resolvers/zod",
      "@tanstack/react-pacer",
      "@tanstack/react-table",
      "@tanstack/react-virtual",
      "react-day-picker",
      "react-day-picker/locale",
      "react-hook-form",
      "react-countdown",
      "react-json-view-lite",
      "vaul",
      "html2canvas-pro",
      "bun",
    ],
  },
  server: {
    port: 3000,
  },
  ssr: {
    external: ["bun"],
    noExternal: [
      "streamdown",
      "@upstash/realtime",
      "lucide-react"
    ],
  },
  build: {
    chunkSizeWarningLimit: 1000, // Set limit to 1000 KB
    rollupOptions: {
      output: {
        minify: true,
      },
    },
  },

  plugins: [
    reflectPolyfillPlugin(),
    devtools(),
    tsConfigPaths({
      projects: ["./tsconfig.json"],
    }),
    postgres({ referrer: "penzo" }),
    tailwindcss(),
    nitro({
      vercel: {
        functions: {
          runtime: "bun1.x",
        },
      },
    }),
    tanstackStart({
      srcDirectory: "src",
      server: { entry: "./server.ts" },
      router: {
        routeToken: "layout",
      },
    }),
    viteReact(),
  ],
});

@CarlosZiegler commented on GitHub (Jan 21, 2026): Hey I added a polifill and works as before: ```typescript import tailwindcss from "@tailwindcss/vite"; import { devtools } from "@tanstack/devtools-vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import { config } from "dotenv"; import { nitro } from "nitro/vite"; import type { Plugin } from "vite"; import { defineConfig } from "vite"; import { postgres } from "vite-plugin-db"; import tsConfigPaths from "vite-tsconfig-paths"; config(); // Polyfill for Reflect.getMetadata (required by @better-auth/passkey) const REFLECT_POLYFILL = ` if (typeof Reflect.getMetadata !== "function") { const m = new WeakMap(); const get = (t, p) => m.get(t)?.get(p); const set = (t, p) => { let a = m.get(t); if (!a) { a = new Map(); m.set(t, a); } let b = a.get(p); if (!b) { b = new Map(); a.set(p, b); } return b; }; const find = (k, t, p) => { const x = get(t, p); if (x?.has(k)) return x.get(k); const pr = Object.getPrototypeOf(t); return pr ? find(k, pr, p) : undefined; }; Reflect.getMetadata = (k, t, p) => find(k, t, p); Reflect.getOwnMetadata = (k, t, p) => get(t, p)?.get(k); Reflect.defineMetadata = (k, v, t, p) => set(t, p).set(k, v); Reflect.hasMetadata = (k, t, p) => find(k, t, p) !== undefined; Reflect.hasOwnMetadata = (k, t, p) => get(t, p)?.has(k) ?? false; Reflect.metadata = (k, v) => (t, p) => set(t, p).set(k, v); } `; function reflectPolyfillPlugin(): Plugin { return { name: "reflect-polyfill", renderChunk(code, chunk) { if (chunk.fileName.includes("passkey")) { return REFLECT_POLYFILL + code; } return null; }, }; } export default defineConfig({ optimizeDeps: { entries: ["src/**/*.{js,jsx,ts,tsx}"], exclude: [ "katex", "pdfjs", "pdf-parse", "qrcode.react", "react-to-print", "@hookform/resolvers/zod", "@tanstack/react-pacer", "@tanstack/react-table", "@tanstack/react-virtual", "react-day-picker", "react-day-picker/locale", "react-hook-form", "react-countdown", "react-json-view-lite", "vaul", "html2canvas-pro", "bun", ], }, server: { port: 3000, }, ssr: { external: ["bun"], noExternal: [ "streamdown", "@upstash/realtime", "lucide-react" ], }, build: { chunkSizeWarningLimit: 1000, // Set limit to 1000 KB rollupOptions: { output: { minify: true, }, }, }, plugins: [ reflectPolyfillPlugin(), devtools(), tsConfigPaths({ projects: ["./tsconfig.json"], }), postgres({ referrer: "penzo" }), tailwindcss(), nitro({ vercel: { functions: { runtime: "bun1.x", }, }, }), tanstackStart({ srcDirectory: "src", server: { entry: "./server.ts" }, router: { routeToken: "layout", }, }), viteReact(), ], }); ```
Author
Owner

@amruthpillai commented on GitHub (Jan 21, 2026):

Brilliant, that works for me as well. Nice idea @CarlosZiegler!

@amruthpillai commented on GitHub (Jan 21, 2026): Brilliant, that works for me as well. Nice idea @CarlosZiegler!
Author
Owner

@tkjaergaard commented on GitHub (Jan 24, 2026):

Hey I added a polifill and works as before:

import tailwindcss from "@tailwindcss/vite";
import { devtools } from "@tanstack/devtools-vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { config } from "dotenv";
import { nitro } from "nitro/vite";
import type { Plugin } from "vite";
import { defineConfig } from "vite";
import { postgres } from "vite-plugin-db";
import tsConfigPaths from "vite-tsconfig-paths";

config();

// Polyfill for Reflect.getMetadata (required by @better-auth/passkey)
const REFLECT_POLYFILL = if (typeof Reflect.getMetadata !== "function") { const m = new WeakMap(); const get = (t, p) => m.get(t)?.get(p); const set = (t, p) => { let a = m.get(t); if (!a) { a = new Map(); m.set(t, a); } let b = a.get(p); if (!b) { b = new Map(); a.set(p, b); } return b; }; const find = (k, t, p) => { const x = get(t, p); if (x?.has(k)) return x.get(k); const pr = Object.getPrototypeOf(t); return pr ? find(k, pr, p) : undefined; }; Reflect.getMetadata = (k, t, p) => find(k, t, p); Reflect.getOwnMetadata = (k, t, p) => get(t, p)?.get(k); Reflect.defineMetadata = (k, v, t, p) => set(t, p).set(k, v); Reflect.hasMetadata = (k, t, p) => find(k, t, p) !== undefined; Reflect.hasOwnMetadata = (k, t, p) => get(t, p)?.has(k) ?? false; Reflect.metadata = (k, v) => (t, p) => set(t, p).set(k, v); };

function reflectPolyfillPlugin(): Plugin {
return {
name: "reflect-polyfill",
renderChunk(code, chunk) {
if (chunk.fileName.includes("passkey")) {
return REFLECT_POLYFILL + code;
}
return null;
},
};
}

export default defineConfig({
optimizeDeps: {
entries: ["src/**/*.{js,jsx,ts,tsx}"],
exclude: [
"katex",
"pdfjs",
"pdf-parse",
"qrcode.react",
"react-to-print",
"@hookform/resolvers/zod",
"@tanstack/react-pacer",
"@tanstack/react-table",
"@tanstack/react-virtual",
"react-day-picker",
"react-day-picker/locale",
"react-hook-form",
"react-countdown",
"react-json-view-lite",
"vaul",
"html2canvas-pro",
"bun",
],
},
server: {
port: 3000,
},
ssr: {
external: ["bun"],
noExternal: [
"streamdown",
"@upstash/realtime",
"lucide-react"
],
},
build: {
chunkSizeWarningLimit: 1000, // Set limit to 1000 KB
rollupOptions: {
output: {
minify: true,
},
},
},

plugins: [
reflectPolyfillPlugin(),
devtools(),
tsConfigPaths({
projects: ["./tsconfig.json"],
}),
postgres({ referrer: "penzo" }),
tailwindcss(),
nitro({
vercel: {
functions: {
runtime: "bun1.x",
},
},
}),
tanstackStart({
srcDirectory: "src",
server: { entry: "./server.ts" },
router: {
routeToken: "layout",
},
}),
viteReact(),
],
});

I had to target tsyringe instead of passkey in order to get it working.

function reflectPolyfillPlugin(): Plugin {
  return {
    name: 'reflect-polyfill',
    renderChunk(code, chunk) {
+     if (chunk.fileName.includes('tsyringe')) {
        return REFLECT_POLYFILL + code
      }
      return null
    },
  }
}
@tkjaergaard commented on GitHub (Jan 24, 2026): > Hey I added a polifill and works as before: > > import tailwindcss from "@tailwindcss/vite"; > import { devtools } from "@tanstack/devtools-vite"; > import { tanstackStart } from "@tanstack/react-start/plugin/vite"; > import viteReact from "@vitejs/plugin-react"; > import { config } from "dotenv"; > import { nitro } from "nitro/vite"; > import type { Plugin } from "vite"; > import { defineConfig } from "vite"; > import { postgres } from "vite-plugin-db"; > import tsConfigPaths from "vite-tsconfig-paths"; > > config(); > > // Polyfill for Reflect.getMetadata (required by @better-auth/passkey) > const REFLECT_POLYFILL = ` > if (typeof Reflect.getMetadata !== "function") { > const m = new WeakMap(); > const get = (t, p) => m.get(t)?.get(p); > const set = (t, p) => { > let a = m.get(t); if (!a) { a = new Map(); m.set(t, a); } > let b = a.get(p); if (!b) { b = new Map(); a.set(p, b); } > return b; > }; > const find = (k, t, p) => { const x = get(t, p); if (x?.has(k)) return x.get(k); const pr = Object.getPrototypeOf(t); return pr ? find(k, pr, p) : undefined; }; > Reflect.getMetadata = (k, t, p) => find(k, t, p); > Reflect.getOwnMetadata = (k, t, p) => get(t, p)?.get(k); > Reflect.defineMetadata = (k, v, t, p) => set(t, p).set(k, v); > Reflect.hasMetadata = (k, t, p) => find(k, t, p) !== undefined; > Reflect.hasOwnMetadata = (k, t, p) => get(t, p)?.has(k) ?? false; > Reflect.metadata = (k, v) => (t, p) => set(t, p).set(k, v); > } > `; > > function reflectPolyfillPlugin(): Plugin { > return { > name: "reflect-polyfill", > renderChunk(code, chunk) { > if (chunk.fileName.includes("passkey")) { > return REFLECT_POLYFILL + code; > } > return null; > }, > }; > } > > export default defineConfig({ > optimizeDeps: { > entries: ["src/**/*.{js,jsx,ts,tsx}"], > exclude: [ > "katex", > "pdfjs", > "pdf-parse", > "qrcode.react", > "react-to-print", > "@hookform/resolvers/zod", > "@tanstack/react-pacer", > "@tanstack/react-table", > "@tanstack/react-virtual", > "react-day-picker", > "react-day-picker/locale", > "react-hook-form", > "react-countdown", > "react-json-view-lite", > "vaul", > "html2canvas-pro", > "bun", > ], > }, > server: { > port: 3000, > }, > ssr: { > external: ["bun"], > noExternal: [ > "streamdown", > "@upstash/realtime", > "lucide-react" > ], > }, > build: { > chunkSizeWarningLimit: 1000, // Set limit to 1000 KB > rollupOptions: { > output: { > minify: true, > }, > }, > }, > > plugins: [ > reflectPolyfillPlugin(), > devtools(), > tsConfigPaths({ > projects: ["./tsconfig.json"], > }), > postgres({ referrer: "penzo" }), > tailwindcss(), > nitro({ > vercel: { > functions: { > runtime: "bun1.x", > }, > }, > }), > tanstackStart({ > srcDirectory: "src", > server: { entry: "./server.ts" }, > router: { > routeToken: "layout", > }, > }), > viteReact(), > ], > }); I had to target `tsyringe` instead of `passkey` in order to get it working. ``` function reflectPolyfillPlugin(): Plugin { return { name: 'reflect-polyfill', renderChunk(code, chunk) { + if (chunk.fileName.includes('tsyringe')) { return REFLECT_POLYFILL + code } return null }, } } ```
Author
Owner

@bytaesu commented on GitHub (Jan 28, 2026):

Probably related to this issue:

@bytaesu commented on GitHub (Jan 28, 2026): Probably related to this issue: - https://github.com/PeculiarVentures/x509/pull/117
Author
Owner

@amruthpillai commented on GitHub (Feb 9, 2026):

Just had to remove the passkeys functionality in my app because of this issue. I really hope it gets a second look.

@amruthpillai commented on GitHub (Feb 9, 2026): Just had to remove the passkeys functionality in my app because of this issue. I really hope it gets a second look.
Author
Owner

@bytaesu commented on GitHub (Feb 9, 2026):

I‘ll reproduce this today, and if there’s anything we can improve, I’ll fix it right away 🧐

@bytaesu commented on GitHub (Feb 9, 2026): I‘ll reproduce this today, and if there’s anything we can improve, I’ll fix it right away 🧐
Author
Owner

@amruthpillai commented on GitHub (Feb 9, 2026):

@bytaesu Thank you so much. If it helps, I set up a minimal reproduction of the issue with no other packages or libraries, just better-auth, its passkeys plugin and @tanstack/start. I have explained the reproduction steps in the README.

https://github.com/amruthpillai/better-auth-passkeys-error

@amruthpillai commented on GitHub (Feb 9, 2026): @bytaesu Thank you so much. If it helps, I set up a minimal reproduction of the issue with no other packages or libraries, just better-auth, its passkeys plugin and @tanstack/start. I have explained the reproduction steps in the README. https://github.com/amruthpillai/better-auth-passkeys-error
Author
Owner

@bytaesu commented on GitHub (Feb 9, 2026):

@amruthpillai Perfect, let me check

@bytaesu commented on GitHub (Feb 9, 2026): @amruthpillai Perfect, let me check
Author
Owner

@bytaesu commented on GitHub (Feb 10, 2026):

Hi @amruthpillai,

This issue seems to occur because Nitro's default behavior tree-shakes reflect-metadata (bare import) into an empty file during production builds. Setting it in Vite like this appears to fix the problem. It doesn't seem appropriate to handle this within Better Auth, as it's likely a Nitro issue 🤔

nitro({
  rollupConfig: {
    treeshake: {
      moduleSideEffects: (id) => {
        if (id.includes('reflect-metadata')) return true;
        // Nitro default configs - https://nitro.build/config#modulesideeffects
        if (id.includes('unenv/polyfill/')) return true;
        if (id.includes('node-fetch-native/polyfill')) return true;
        return false;
      },
    },
  },
}),
Image
@bytaesu commented on GitHub (Feb 10, 2026): Hi @amruthpillai, This issue seems to occur because Nitro's default behavior tree-shakes `reflect-metadata` (bare import) into an empty file during production builds. Setting it in Vite like this appears to fix the problem. It doesn't seem appropriate to handle this within Better Auth, as it's likely a Nitro issue 🤔 ```ts nitro({ rollupConfig: { treeshake: { moduleSideEffects: (id) => { if (id.includes('reflect-metadata')) return true; // Nitro default configs - https://nitro.build/config#modulesideeffects if (id.includes('unenv/polyfill/')) return true; if (id.includes('node-fetch-native/polyfill')) return true; return false; }, }, }, }), ``` <img width="1386" height="660" alt="Image" src="https://github.com/user-attachments/assets/e5300dd9-8972-4d15-ab58-29d2c62cada1" />
Author
Owner

@bytaesu commented on GitHub (Feb 10, 2026):

+) I updated the TanStack integration guide which is in the canary for now https://canary.better-auth.com/docs/integrations/tanstack

The current middleware session check can be bypassed by client-side navigation like <Link>, so I recommend following the new guide using the beforeLoad approach.

@bytaesu commented on GitHub (Feb 10, 2026): +) I updated the TanStack integration guide which is in the canary for now https://canary.better-auth.com/docs/integrations/tanstack The current `middleware` session check can be bypassed by client-side navigation like `<Link>`, so I recommend following the new guide using the `beforeLoad` approach.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2731