[GH-ISSUE #4250] The inferred type of 'auth' cannot be named without a reference #9872

Closed
opened 2026-04-13 05:39:42 -05:00 by GiteaMirror · 38 comments
Owner

Originally created by @rinarakaki on GitHub (Aug 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4250

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Could you please help with this PR: https://github.com/better-auth/better-auth/issues/2123? The problem hasn't been solved yet.

Current vs. Expected behavior

Same.

What version of Better Auth are you using?

1.3.7

System info

N/A

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

Types

Auth config (if applicable)


Additional context

No response

Originally created by @rinarakaki on GitHub (Aug 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4250 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Could you please help with this PR: https://github.com/better-auth/better-auth/issues/2123? The problem hasn't been solved yet. ### Current vs. Expected behavior Same. ### What version of Better Auth are you using? 1.3.7 ### System info ```bash N/A ``` ### Which area(s) are affected? (Select all that apply) Types ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 05:39:42 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 27, 2025):

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

This TypeScript error happens because the inferred type for auth is too complex or anonymous for TypeScript to serialize and export, especially when using plugins or sharing the auth instance across modules or packages. The best way to resolve this is to use an explicit type annotation when exporting auth. Here’s a recommended pattern:

import { betterAuth, BetterAuthOptions } from "better-auth";

const authConfig = {
  // your config here
} satisfies BetterAuthOptions;

export const auth = betterAuth(authConfig) as ReturnType<typeof betterAuth<typeof authConfig>>;

This approach gives TypeScript a concrete type to reference, making the type portable and avoiding the error. If you’re in a monorepo or exporting auth from a shared package, this is especially important.

If you’re using plugins, make sure to include them directly in the config and avoid type assertions like admin() as unknown as BetterAuthPlugin, as these can break type inference for plugin-added types. If you need to type auth.api, you can do so with Auth['api'] after defining type Auth = ReturnType<typeof betterAuth<typeof authConfig>> example.

Other workarounds (not ideal for all projects) include setting "declaration": false and "declarationMap": false in your tsconfig.json example, but this disables type declarations.

If you’re still seeing issues, double-check your plugin imports—incorrect type imports in plugins have caused this error before example.

Let me know if you want to share your specific config or setup for more targeted help!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3226530352 --> @dosubot[bot] commented on GitHub (Aug 27, 2025): <!-- Greeting --> Hi @rinarakaki! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This TypeScript error happens because the inferred type for `auth` is too complex or anonymous for TypeScript to serialize and export, especially when using plugins or sharing the `auth` instance across modules or packages. The best way to resolve this is to use an explicit type annotation when exporting `auth`. Here’s a recommended pattern: ```ts import { betterAuth, BetterAuthOptions } from "better-auth"; const authConfig = { // your config here } satisfies BetterAuthOptions; export const auth = betterAuth(authConfig) as ReturnType<typeof betterAuth<typeof authConfig>>; ``` This approach gives TypeScript a concrete type to reference, making the type portable and avoiding the error. If you’re in a monorepo or exporting `auth` from a shared package, this is especially important. If you’re using plugins, make sure to include them directly in the config and avoid type assertions like `admin() as unknown as BetterAuthPlugin`, as these can break type inference for plugin-added types. If you need to type `auth.api`, you can do so with `Auth['api']` after defining `type Auth = ReturnType<typeof betterAuth<typeof authConfig>>` [example](https://github.com/better-auth/better-auth/issues/3015). Other workarounds (not ideal for all projects) include setting `"declaration": false` and `"declarationMap": false` in your `tsconfig.json` [example](https://github.com/better-auth/better-auth/issues/2123), but this disables type declarations. If you’re still seeing issues, double-check your plugin imports—incorrect type imports in plugins have caused this error before [example](https://github.com/better-auth/better-auth/issues/2413). Let me know if you want to share your specific config or setup for more targeted help! <!-- 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/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/14aec2ca-33e8-4ca8-ba31-dfab5327b5d2?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/4250)
Author
Owner

@Kinfe123 commented on GitHub (Aug 27, 2025):

can you please share you auth config and tsconfig file ?

<!-- gh-comment-id:3227553041 --> @Kinfe123 commented on GitHub (Aug 27, 2025): can you please share you auth config and tsconfig file ?
Author
Owner

@rinarakaki commented on GitHub (Aug 30, 2025):

Repo structure:

  • apps
    • api
  • packages
    • magic-link ← custom plugin

Auth config:

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { magicLink } from "magic-link";
import { prisma } from "prisma";

export const auth = betterAuth({
  baseURL: "http://localhost:3002/auth",
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  plugins: [
    magicLink({}),
  ],
});

apps/api/tsconfig.json

{
  "extends": "tsconfig/tsconfig.base.json",
  "compilerOptions": {
    "module": "NodeNext",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "ES2021",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "paths": {
      "src/*": ["./src/*"]
    },
    "incremental": true,
    "skipLibCheck": true,
    "strictBindCallApply": false,
    "moduleResolution": "NodeNext",
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": false,
    "noUncheckedIndexedAccess": true,
    "allowJs": true,
    "resolveJsonModule": true,
    "types": ["@types/jest", "@quramy/jest-prisma-node"]
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

packages/magic-link/tsconfig.json

{
  "extends": "tsconfig/tsconfig.base.json",
  "compilerOptions": {
    "target": "es2020",
    "lib": ["esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "dist"]
}
<!-- gh-comment-id:3239112185 --> @rinarakaki commented on GitHub (Aug 30, 2025): Repo structure: - apps - api - packages - magic-link ← custom plugin Auth config: ```ts import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { magicLink } from "magic-link"; import { prisma } from "prisma"; export const auth = betterAuth({ baseURL: "http://localhost:3002/auth", database: prismaAdapter(prisma, { provider: "postgresql", }), plugins: [ magicLink({}), ], }); ``` apps/api/tsconfig.json ```json { "extends": "tsconfig/tsconfig.base.json", "compilerOptions": { "module": "NodeNext", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "paths": { "src/*": ["./src/*"] }, "incremental": true, "skipLibCheck": true, "strictBindCallApply": false, "moduleResolution": "NodeNext", "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": false, "noUncheckedIndexedAccess": true, "allowJs": true, "resolveJsonModule": true, "types": ["@types/jest", "@quramy/jest-prisma-node"] }, "include": ["**/*.ts"], "exclude": ["node_modules", "dist"] } ``` packages/magic-link/tsconfig.json ```json { "extends": "tsconfig/tsconfig.base.json", "compilerOptions": { "target": "es2020", "lib": ["esnext"], "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "noUncheckedIndexedAccess": true }, "include": ["**/*.ts"], "exclude": ["node_modules", "dist"] } ```
Author
Owner

@rinarakaki commented on GitHub (Aug 30, 2025):

Setting "declaration" to false or simply removing it solved the problem.

<!-- gh-comment-id:3239148690 --> @rinarakaki commented on GitHub (Aug 30, 2025): Setting "declaration" to false or simply removing it solved the problem.
Author
Owner

@Kinfe123 commented on GitHub (Aug 30, 2025):

It should be because of the magic link custom plugin not having the right plugin type which makes it a harder a typescript to infer the type when emitting dts file.

<!-- gh-comment-id:3239185122 --> @Kinfe123 commented on GitHub (Aug 30, 2025): It should be because of the magic link custom plugin not having the right plugin type which makes it a harder a typescript to infer the type when emitting dts file.
Author
Owner

@Kinfe123 commented on GitHub (Aug 30, 2025):

Have edited it. There was a typo

<!-- gh-comment-id:3239231284 --> @Kinfe123 commented on GitHub (Aug 30, 2025): Have edited it. There was a typo
Author
Owner

@rinarakaki commented on GitHub (Aug 30, 2025):

Actually, another problem came up:

src/index.ts(59,14): error TS2742: The inferred type of 'magicLink' cannot be named without a reference to '../../../node_modules/better-call/dist/router-c8myTmx-.cjs'. This is likely not portable. A type annotation is necessary.
<!-- gh-comment-id:3239231389 --> @rinarakaki commented on GitHub (Aug 30, 2025): Actually, another problem came up: ``` src/index.ts(59,14): error TS2742: The inferred type of 'magicLink' cannot be named without a reference to '../../../node_modules/better-call/dist/router-c8myTmx-.cjs'. This is likely not portable. A type annotation is necessary. ```
Author
Owner

@Kinfe123 commented on GitHub (Aug 30, 2025):

Yeah can you please share you plugin code to make it easier to debug this is like because of multiple reason for ts not getting your dts file named and make sure they inferred correctly when declaration enabled

<!-- gh-comment-id:3239232253 --> @Kinfe123 commented on GitHub (Aug 30, 2025): Yeah can you please share you plugin code to make it easier to debug this is like because of multiple reason for ts not getting your dts file named and make sure they inferred correctly when declaration enabled
Author
Owner

@rinarakaki commented on GitHub (Aug 30, 2025):

Is it possible that the problem is because I'm using kind of internal better-auth APIs?:

packages/magic-link/src/index.ts

import { createAuthEndpoint, originCheck } from "better-auth/api";
import { setSessionCookie } from "better-auth/cookies";
import { generateRandomString } from "better-auth/crypto";
import type { BetterAuthPlugin, GenericEndpointContext } from "better-auth/types";
import { APIError } from "better-call";
import * as z from "zod/v4";

...

unlike https://github.com/better-auth/better-auth/blob/canary/packages/better-auth/src/plugins/magic-link/index.ts

<!-- gh-comment-id:3239232883 --> @rinarakaki commented on GitHub (Aug 30, 2025): Is it possible that the problem is because I'm using kind of internal better-auth APIs?: packages/magic-link/src/index.ts ```ts import { createAuthEndpoint, originCheck } from "better-auth/api"; import { setSessionCookie } from "better-auth/cookies"; import { generateRandomString } from "better-auth/crypto"; import type { BetterAuthPlugin, GenericEndpointContext } from "better-auth/types"; import { APIError } from "better-call"; import * as z from "zod/v4"; ... ``` unlike https://github.com/better-auth/better-auth/blob/canary/packages/better-auth/src/plugins/magic-link/index.ts
Author
Owner

@rinarakaki commented on GitHub (Aug 30, 2025):

The full code:

packages/magic-link/src/index.ts

import { createAuthEndpoint, originCheck } from "better-auth/api";
import { setSessionCookie } from "better-auth/cookies";
import { generateRandomString } from "better-auth/crypto";
import type { BetterAuthPlugin, GenericEndpointContext } from "better-auth/types";
import { APIError } from "better-call";
import * as z from "zod/v4";

import { defaultKeyHasher } from "./utils";

interface MagicLinkopts {
  /**
   * Time in seconds until the magic link expires.
   * @default (60 * 5) // 5 minutes
   */
  expiresIn?: number;
  /**
   * Rate limit configuration.
   *
   * @default {
   *  window: 60,
   *  max: 5,
   * }
   */
  rateLimit?: {
    window: number;
    max: number;
  };
  /**
   * This option allows you to configure how the token is stored in your database.
   * Note: This will not affect the token that's sent, it will only affect the token stored in your database.
   *
   * @default "plain"
   */
  storeToken?: "plain" | "hashed" | { type: "custom-hasher"; hash: (token: string) => Promise<string> };
}

export function magicLink(options: MagicLinkopts) {
  const opts = {
    storeToken: "plain",
    ...options,
  } satisfies MagicLinkopts;

  async function storeToken(ctx: GenericEndpointContext, token: string) {
    if (opts.storeToken === "hashed") {
      return await defaultKeyHasher(token);
    }
    if (typeof opts.storeToken === "object" && "type" in opts.storeToken && opts.storeToken.type === "custom-hasher") {
      return await opts.storeToken.hash(token);
    }
    return token;
  }

  return {
    id: "magic-link",
    endpoints: {
      /**
       * ### Endpoint
       *
       * POST `/sign-in/magic-link`
       *
       * ### API Methods
       *
       * **server:**
       * `auth.api.sendMagicLink`
       *
       * **client:**
       * `authClient.signIn.magicLink`
       *
       * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-magic-link)
       */
      sendMagicLink: createAuthEndpoint(
        "/sign-in/magic-link",
        {
          method: "POST",
          requireHeaders: true,
          body: z.object({
            userId: z.string().meta({
              description: "User ID to send the magic link to",
            }),
            callbackURL: z
              .string()
              .meta({
                description: "URL to redirect after magic link verification",
              })
              .optional(),
            errorCallbackURL: z
              .string()
              .meta({
                description: "URL to redirect after error.",
              })
              .optional(),
          }),
          metadata: {
            openapi: {
              description: "Sign in with magic link",
              responses: {
                200: {
                  description: "Success",
                  content: {
                    "application/json": {
                      schema: {
                        type: "object",
                        properties: {
                          url: {
                            type: "string",
                          },
                          token: {
                            type: "string",
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        },
        async (ctx) => {
          const session = ctx.context.session;

          const user = session?.user;

          if (!user) {
            throw new APIError("BAD_REQUEST", {
              message: "User not found",
            });
          }

          const token = generateRandomString(32, "a-z", "A-Z");
          const storedToken = await storeToken(ctx, token);
          await ctx.context.internalAdapter.createVerificationValue(
            {
              identifier: storedToken,
              value: JSON.stringify({ userId: user.id }),
              expiresAt: new Date(Date.now() + (opts.expiresIn || 60 * 5) * 1000),
            },
            ctx,
          );
          const realBaseURL = new URL(ctx.context.baseURL);
          const pathname = realBaseURL.pathname === "/" ? "" : realBaseURL.pathname;
          const basePath = pathname ? "" : ctx.context.options.basePath || "";
          const url = new URL(`${pathname}${basePath}/magic-link/verify`, realBaseURL.origin);
          url.searchParams.set("token", token);
          url.searchParams.set("callbackURL", ctx.body.callbackURL || "/");
          if (ctx.body.errorCallbackURL) {
            url.searchParams.set("errorCallbackURL", ctx.body.errorCallbackURL);
          }
          return ctx.json({
            url: url.toString(),
            token,
          });
        },
      ),
      /**
       * ### Endpoint
       *
       * GET `/magic-link/verify`
       *
       * ### API Methods
       *
       * **server:**
       * `auth.api.magicLinkVerify`
       *
       * **client:**
       * `authClient.magicLink.verify`
       *
       * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/magic-link#api-method-magic-link-verify)
       */
      magicLinkVerify: createAuthEndpoint(
        "/magic-link/verify",
        {
          method: "GET",
          query: z.object({
            token: z.string().meta({
              description: "Verification token",
            }),
            callbackURL: z
              .string()
              .meta({
                description:
                  'URL to redirect after magic link verification, if not provided the user will be redirected to the root URL. Eg: "/dashboard"',
              })
              .optional(),
            errorCallbackURL: z
              .string()
              .meta({
                description: "URL to redirect after error.",
              })
              .optional(),
            newUserCallbackURL: z
              .string()
              .meta({
                description:
                  "URL to redirect after new user signup. Only used if the user is registering for the first time.",
              })
              .optional(),
          }),
          use: [
            originCheck((ctx) => {
              return ctx.query.callbackURL ? decodeURIComponent(ctx.query.callbackURL) : "/";
            }),
            originCheck((ctx) => {
              return ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : "/";
            }),
            originCheck((ctx) => {
              return ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : "/";
            }),
          ],
          requireHeaders: true,
          metadata: {
            openapi: {
              description: "Verify magic link",
              responses: {
                200: {
                  description: "Success",
                  content: {
                    "application/json": {
                      schema: {
                        type: "object",
                        properties: {
                          session: {
                            $ref: "#/components/schemas/Session",
                          },
                          user: {
                            $ref: "#/components/schemas/User",
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        },
        async (ctx) => {
          const token = ctx.query.token;
          // If the first argument provides the origin, it will ignore the second argument of `new URL`.
          // new URL("http://localhost:3001/hello", "http://localhost:3000").toString()
          // Returns http://localhost:3001/hello
          const callbackURL = new URL(
            ctx.query.callbackURL ? decodeURIComponent(ctx.query.callbackURL) : "/",
            ctx.context.baseURL,
          ).toString();
          const errorCallbackURL = new URL(
            ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : callbackURL,
            ctx.context.baseURL,
          ).toString();
          const storedToken = await storeToken(ctx, token);
          const tokenValue = await ctx.context.internalAdapter.findVerificationValue(storedToken);
          if (!tokenValue) {
            throw ctx.redirect(`${errorCallbackURL}?error=INVALID_TOKEN`);
          }
          if (tokenValue.expiresAt < new Date()) {
            await ctx.context.internalAdapter.deleteVerificationValue(tokenValue.id);
            throw ctx.redirect(`${errorCallbackURL}?error=EXPIRED_TOKEN`);
          }
          await ctx.context.internalAdapter.deleteVerificationValue(tokenValue.id);
          const { userId } = JSON.parse(tokenValue.value) as {
            userId: string;
          };
          const user = await ctx.context.internalAdapter.findUserById(userId);

          if (!user) {
            throw ctx.redirect(`${errorCallbackURL}?error=new_user_signup_disabled`);
          }

          const session = await ctx.context.internalAdapter.createSession(user.id, ctx);

          if (!session) {
            throw ctx.redirect(`${errorCallbackURL}?error=failed_to_create_session`);
          }

          await setSessionCookie(ctx, {
            session,
            user,
          });
          if (!ctx.query.callbackURL) {
            return ctx.json({
              token: session.token,
              user: {
                id: user.id,
                email: user.email,
                emailVerified: user.emailVerified,
                name: user.name,
                image: user.image,
                createdAt: user.createdAt,
                updatedAt: user.updatedAt,
              },
            });
          }
          throw ctx.redirect(callbackURL);
        },
      ),
    },
    rateLimit: [
      {
        pathMatcher(path) {
          return path.startsWith("/sign-in/magic-link") || path.startsWith("/magic-link/verify");
        },
        window: opts.rateLimit?.window || 60,
        max: opts.rateLimit?.max || 5,
      },
    ],
  } satisfies BetterAuthPlugin;
}

packages/magic-link/src/client.ts

import type { BetterAuthClientPlugin } from "better-auth/client";
import type { magicLink } from ".";

export function magicLinkClient(): BetterAuthClientPlugin {
  return {
    id: "magic-link",
    $InferServerPlugin: {} as ReturnType<typeof magicLink>,
  } satisfies BetterAuthClientPlugin;
}

packages/magic-link/src/utils.ts

import { base64Url } from "@better-auth/utils/base64";
import { createHash } from "@better-auth/utils/hash";

export const defaultKeyHasher = async (otp: string) => {
  const hash = await createHash("SHA-256").digest(
    new TextEncoder().encode(otp),
  );
  const hashed = base64Url.encode(new Uint8Array(hash), {
    padding: false,
  });
  return hashed;
};
<!-- gh-comment-id:3239234695 --> @rinarakaki commented on GitHub (Aug 30, 2025): The full code: packages/magic-link/src/index.ts ```ts import { createAuthEndpoint, originCheck } from "better-auth/api"; import { setSessionCookie } from "better-auth/cookies"; import { generateRandomString } from "better-auth/crypto"; import type { BetterAuthPlugin, GenericEndpointContext } from "better-auth/types"; import { APIError } from "better-call"; import * as z from "zod/v4"; import { defaultKeyHasher } from "./utils"; interface MagicLinkopts { /** * Time in seconds until the magic link expires. * @default (60 * 5) // 5 minutes */ expiresIn?: number; /** * Rate limit configuration. * * @default { * window: 60, * max: 5, * } */ rateLimit?: { window: number; max: number; }; /** * This option allows you to configure how the token is stored in your database. * Note: This will not affect the token that's sent, it will only affect the token stored in your database. * * @default "plain" */ storeToken?: "plain" | "hashed" | { type: "custom-hasher"; hash: (token: string) => Promise<string> }; } export function magicLink(options: MagicLinkopts) { const opts = { storeToken: "plain", ...options, } satisfies MagicLinkopts; async function storeToken(ctx: GenericEndpointContext, token: string) { if (opts.storeToken === "hashed") { return await defaultKeyHasher(token); } if (typeof opts.storeToken === "object" && "type" in opts.storeToken && opts.storeToken.type === "custom-hasher") { return await opts.storeToken.hash(token); } return token; } return { id: "magic-link", endpoints: { /** * ### Endpoint * * POST `/sign-in/magic-link` * * ### API Methods * * **server:** * `auth.api.sendMagicLink` * * **client:** * `authClient.signIn.magicLink` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-magic-link) */ sendMagicLink: createAuthEndpoint( "/sign-in/magic-link", { method: "POST", requireHeaders: true, body: z.object({ userId: z.string().meta({ description: "User ID to send the magic link to", }), callbackURL: z .string() .meta({ description: "URL to redirect after magic link verification", }) .optional(), errorCallbackURL: z .string() .meta({ description: "URL to redirect after error.", }) .optional(), }), metadata: { openapi: { description: "Sign in with magic link", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { url: { type: "string", }, token: { type: "string", }, }, }, }, }, }, }, }, }, }, async (ctx) => { const session = ctx.context.session; const user = session?.user; if (!user) { throw new APIError("BAD_REQUEST", { message: "User not found", }); } const token = generateRandomString(32, "a-z", "A-Z"); const storedToken = await storeToken(ctx, token); await ctx.context.internalAdapter.createVerificationValue( { identifier: storedToken, value: JSON.stringify({ userId: user.id }), expiresAt: new Date(Date.now() + (opts.expiresIn || 60 * 5) * 1000), }, ctx, ); const realBaseURL = new URL(ctx.context.baseURL); const pathname = realBaseURL.pathname === "/" ? "" : realBaseURL.pathname; const basePath = pathname ? "" : ctx.context.options.basePath || ""; const url = new URL(`${pathname}${basePath}/magic-link/verify`, realBaseURL.origin); url.searchParams.set("token", token); url.searchParams.set("callbackURL", ctx.body.callbackURL || "/"); if (ctx.body.errorCallbackURL) { url.searchParams.set("errorCallbackURL", ctx.body.errorCallbackURL); } return ctx.json({ url: url.toString(), token, }); }, ), /** * ### Endpoint * * GET `/magic-link/verify` * * ### API Methods * * **server:** * `auth.api.magicLinkVerify` * * **client:** * `authClient.magicLink.verify` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/magic-link#api-method-magic-link-verify) */ magicLinkVerify: createAuthEndpoint( "/magic-link/verify", { method: "GET", query: z.object({ token: z.string().meta({ description: "Verification token", }), callbackURL: z .string() .meta({ description: 'URL to redirect after magic link verification, if not provided the user will be redirected to the root URL. Eg: "/dashboard"', }) .optional(), errorCallbackURL: z .string() .meta({ description: "URL to redirect after error.", }) .optional(), newUserCallbackURL: z .string() .meta({ description: "URL to redirect after new user signup. Only used if the user is registering for the first time.", }) .optional(), }), use: [ originCheck((ctx) => { return ctx.query.callbackURL ? decodeURIComponent(ctx.query.callbackURL) : "/"; }), originCheck((ctx) => { return ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : "/"; }), originCheck((ctx) => { return ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : "/"; }), ], requireHeaders: true, metadata: { openapi: { description: "Verify magic link", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { session: { $ref: "#/components/schemas/Session", }, user: { $ref: "#/components/schemas/User", }, }, }, }, }, }, }, }, }, }, async (ctx) => { const token = ctx.query.token; // If the first argument provides the origin, it will ignore the second argument of `new URL`. // new URL("http://localhost:3001/hello", "http://localhost:3000").toString() // Returns http://localhost:3001/hello const callbackURL = new URL( ctx.query.callbackURL ? decodeURIComponent(ctx.query.callbackURL) : "/", ctx.context.baseURL, ).toString(); const errorCallbackURL = new URL( ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : callbackURL, ctx.context.baseURL, ).toString(); const storedToken = await storeToken(ctx, token); const tokenValue = await ctx.context.internalAdapter.findVerificationValue(storedToken); if (!tokenValue) { throw ctx.redirect(`${errorCallbackURL}?error=INVALID_TOKEN`); } if (tokenValue.expiresAt < new Date()) { await ctx.context.internalAdapter.deleteVerificationValue(tokenValue.id); throw ctx.redirect(`${errorCallbackURL}?error=EXPIRED_TOKEN`); } await ctx.context.internalAdapter.deleteVerificationValue(tokenValue.id); const { userId } = JSON.parse(tokenValue.value) as { userId: string; }; const user = await ctx.context.internalAdapter.findUserById(userId); if (!user) { throw ctx.redirect(`${errorCallbackURL}?error=new_user_signup_disabled`); } const session = await ctx.context.internalAdapter.createSession(user.id, ctx); if (!session) { throw ctx.redirect(`${errorCallbackURL}?error=failed_to_create_session`); } await setSessionCookie(ctx, { session, user, }); if (!ctx.query.callbackURL) { return ctx.json({ token: session.token, user: { id: user.id, email: user.email, emailVerified: user.emailVerified, name: user.name, image: user.image, createdAt: user.createdAt, updatedAt: user.updatedAt, }, }); } throw ctx.redirect(callbackURL); }, ), }, rateLimit: [ { pathMatcher(path) { return path.startsWith("/sign-in/magic-link") || path.startsWith("/magic-link/verify"); }, window: opts.rateLimit?.window || 60, max: opts.rateLimit?.max || 5, }, ], } satisfies BetterAuthPlugin; } ``` packages/magic-link/src/client.ts ```ts import type { BetterAuthClientPlugin } from "better-auth/client"; import type { magicLink } from "."; export function magicLinkClient(): BetterAuthClientPlugin { return { id: "magic-link", $InferServerPlugin: {} as ReturnType<typeof magicLink>, } satisfies BetterAuthClientPlugin; } ``` packages/magic-link/src/utils.ts ```ts import { base64Url } from "@better-auth/utils/base64"; import { createHash } from "@better-auth/utils/hash"; export const defaultKeyHasher = async (otp: string) => { const hash = await createHash("SHA-256").digest( new TextEncoder().encode(otp), ); const hashed = base64Url.encode(new Uint8Array(hash), { padding: false, }); return hashed; }; ```
Author
Owner

@picardplaisimond commented on GitHub (Aug 31, 2025):

Same problem:

The inferred type of 'auth' cannot be named without a reference to '.pnpm/zod@4.1.5/node_modules/zod'. This is likely not portable. A type annotation is necessary.ts(2742)
The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.ts(7056)

I’m pretty sure this comes from a tsconfig issue, but I still need to investigate further.
When I set declaration from true to false in the tsconfig, the problem disappears — but unfortunately I still need that setting enabled.

<!-- gh-comment-id:3240028602 --> @picardplaisimond commented on GitHub (Aug 31, 2025): Same problem: ``` The inferred type of 'auth' cannot be named without a reference to '.pnpm/zod@4.1.5/node_modules/zod'. This is likely not portable. A type annotation is necessary.ts(2742) The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.ts(7056) ``` I’m pretty sure this comes from a tsconfig issue, but I still need to investigate further. When I set declaration from true to false in the tsconfig, the problem disappears — but unfortunately I still need that setting enabled.
Author
Owner

@Kinfe123 commented on GitHub (Sep 2, 2025):

@rinarakaki where is sendMagicLink callback that you have showed on your auth config ?

<!-- gh-comment-id:3245319959 --> @Kinfe123 commented on GitHub (Sep 2, 2025): @rinarakaki where is sendMagicLink callback that you have showed on your auth config ?
Author
Owner

@rinarakaki commented on GitHub (Sep 2, 2025):

Updated auth config. You can ignore the callback.

<!-- gh-comment-id:3245533825 --> @rinarakaki commented on GitHub (Sep 2, 2025): Updated auth config. You can ignore the callback.
Author
Owner

@Kinfe123 commented on GitHub (Sep 2, 2025):

The callback might still affect on the way ts emit the types for function signiture when declaration set to true since it is part of the options. or are you actually using the callback ? or it is just there for the example.

<!-- gh-comment-id:3245679418 --> @Kinfe123 commented on GitHub (Sep 2, 2025): The callback might still affect on the way ts emit the types for function signiture when declaration set to `true` since it is part of the options. or are you actually using the callback ? or it is just there for the example.
Author
Owner

@Kinfe123 commented on GitHub (Sep 2, 2025):

@picardplaisimond are you using organization or admin plugin ?

<!-- gh-comment-id:3245764314 --> @Kinfe123 commented on GitHub (Sep 2, 2025): @picardplaisimond are you using organization or admin plugin ?
Author
Owner

@ghassenbenghorbal commented on GitHub (Sep 2, 2025):

The error happens when you use the organization plugin

<!-- gh-comment-id:3246486656 --> @ghassenbenghorbal commented on GitHub (Sep 2, 2025): The error happens when you use the organization plugin
Author
Owner

@picardplaisimond commented on GitHub (Sep 3, 2025):

@picardplaisimond are you using organization or admin plugin ?

I do use the admin plugin, with the username plugin.

I fixed the error with ReturnType<typeof betterAuth>. I think this should be in the doc because it's not fun to search around for a fix to this. But it's an esay fix and it does work really well for now.

<!-- gh-comment-id:3247507146 --> @picardplaisimond commented on GitHub (Sep 3, 2025): > [@picardplaisimond](https://github.com/picardplaisimond) are you using organization or admin plugin ? I do use the `admin` plugin, with the `username` plugin. I fixed the error with `ReturnType<typeof betterAuth>`. I think this should be in the doc because it's not fun to search around for a fix to this. But it's an esay fix and it does work really well for now.
Author
Owner

@Kinfe123 commented on GitHub (Sep 3, 2025):

that is like avoiding the load for ts for not worry about inferring the type since u assert it. thats why and doing loss also drop some fields not to inferred correctly like some fields in session that are added becasuse of the plugin is added like activeOrganizationId and such

<!-- gh-comment-id:3248682601 --> @Kinfe123 commented on GitHub (Sep 3, 2025): that is like avoiding the load for ts for not worry about inferring the type since u assert it. thats why and doing loss also drop some fields not to inferred correctly like some fields in session that are added becasuse of the plugin is added like `activeOrganizationId` and such
Author
Owner

@picardplaisimond commented on GitHub (Sep 5, 2025):

I don’t want to add noise to this conversation, but it seems that only few users are encountering this error. I think it’s important to point out that I was working in a monorepo with pnpm and turbo. Based on the error messages I got, the type inference was conflicting between better-auth and the zod package.

<!-- gh-comment-id:3256631441 --> @picardplaisimond commented on GitHub (Sep 5, 2025): I don’t want to add noise to this conversation, but it seems that only few users are encountering this error. I think it’s important to point out that I was working in a `monorepo` with `pnpm` and `turbo`. Based on the error messages I got, the type inference was conflicting between `better-auth` and the `zod` package.
Author
Owner

@Jordan-Hall commented on GitHub (Sep 8, 2025):

admin plugin, with the username plugin.

I fixed the error with ReturnT

Where did you add this. Im experiencing same issue in tubro mono repo

<!-- gh-comment-id:3265929575 --> @Jordan-Hall commented on GitHub (Sep 8, 2025): > `admin` plugin, with the `username` plugin. > > I fixed the error with `ReturnT` Where did you add this. Im experiencing same issue in tubro mono repo
Author
Owner

@jakst commented on GitHub (Sep 11, 2025):

We've got a monorepo with Typescript project references. I keep running into problems like these with better-auth. It's especially tricky because we need a factory function to create our auth client, since we need to tie into other services in different life cycle hooks. The factory function makes it impossible to work around the problem like dosubot suggested.

This pretty much makes it impossible to get the client properly inferred. I'ts a real pain 😖

<!-- gh-comment-id:3279554147 --> @jakst commented on GitHub (Sep 11, 2025): We've got a monorepo with Typescript project references. I keep running into problems like these with better-auth. It's especially tricky because we need a factory function to create our auth client, since we need to tie into other services in different life cycle hooks. The factory function makes it impossible to work around the problem like dosubot suggested. This pretty much makes it impossible to get the client properly inferred. I'ts a real pain 😖
Author
Owner

@Natsume-197 commented on GitHub (Sep 20, 2025):

The solution proposed by Dosubot https://github.com/better-auth/better-auth/issues/4250#issuecomment-3226530352 worked perfectly for me, thank you!

I think this fix, while not perfect, It is important enough to be included in the documentation.

<!-- gh-comment-id:3314809458 --> @Natsume-197 commented on GitHub (Sep 20, 2025): The solution proposed by Dosubot https://github.com/better-auth/better-auth/issues/4250#issuecomment-3226530352 worked perfectly for me, thank you! I think this fix, while not perfect, It is important enough to be included in the documentation.
Author
Owner

@Garnet-Liu commented on GitHub (Sep 20, 2025):

The solution proposed by Dosubot #4250 (comment) worked perfectly for me, thank you!

I think this fix, while not perfect, It is important enough to be included in the documentation.

import { betterAuth, BetterAuthOptions } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { bearer, customSession, getJwtToken, jwt, openAPI } from "better-auth/plugins";

import { authPrisma } from "@/lib/auth-prisma";

const options = {
  trustedOrigins: (process.env.BETTER_TRUSTED_ORIGINS ?? "").split(",").filter(Boolean),
  database: prismaAdapter(authPrisma, { provider: "sqlite" }),
  plugins: [openAPI(), bearer(), jwt()],
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    github: {
      clientId: process.env.BETTER_GITHUB_CLIENT_ID as string,
      clientSecret: process.env.BETTER_GITHUB_CLIENT_SECRET as string,
    },
    google: {
      enabled: true,
      clientId: process.env.BETTER_GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.BETTER_GOOGLE_CLIENT_SECRET as string,
    },
  },
  advanced: {
    cookiePrefix: "vuer-auth",
    useSecureCookies: process.env.NODE_ENV === "production",
  },
} satisfies BetterAuthOptions;

export const auth = betterAuth({
  ...options,
  plugins: [
    ...options.plugins,
    customSession(
      async ({ user, session }, ctx) => {
        console.log("================= customSession after =================");
        console.log(ctx.context.returned);
        console.log(ctx.context.responseHeaders);

        const jwt = await getJwtToken(ctx, undefined);
        return { user, session: { ...session, jwt } };
      },
      options,
      { shouldMutateListDeviceSessionsEndpoint: true },
    ),
  ],
}) as ReturnType<typeof betterAuth<typeof options>>;

TS2352: Conversion of type
{ handler: (request: Request) => Promise<Response>; api: import("/Users/garnet/projects/mewtwochips/node_modules/.pnpm/better-auth@1.3.11_next@15.4.7_@babel+core@7.28.4_react-dom@19.1.1_react@19.1.1__react@_e1647ea21c6ddce493c9a65686cc263e/node_modules/better-auth/dist/shared/better-auth.BBLxGH6k").h<{ ok: { <AsResp...
to type
{ handler: (request: Request) => Promise<Response>; api: import("/Users/garnet/projects/mewtwochips/node_modules/.pnpm/better-auth@1.3.11_next@15.4.7_@babel+core@7.28.4_react-dom@19.1.1_react@19.1.1__react@_e1647ea21c6ddce493c9a65686cc263e/node_modules/better-auth/dist/shared/better-auth.BBLxGH6k").h<{ ok: { <AsResp...
may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to unknown first.
The types of options.plugins are incompatible between these types.
Type '[{ id: "open-api"; endpoints: { generateOpenAPISchema: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({ body?: undefined; } & { method?: "GET" | undefined; } & { query?: Record<string, any> | undefined; } & ... 4 more ... & { ...; }) | undefined): Promise<...>; options: {...' is not comparable to type '[{ id: "open-api"; endpoints: { generateOpenAPISchema: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({ body?: undefined; } & { method?: "GET" | undefined; } & { query?: Record<string, any> | undefined; } & ... 4 more ... & { ...; }) | undefined): Promise<...>; options: {...'.
Source has 4 element(s) but target allows only 3

Even after doing this, I still encountered this error.

<!-- gh-comment-id:3314997777 --> @Garnet-Liu commented on GitHub (Sep 20, 2025): > The solution proposed by Dosubot [#4250 (comment)](https://github.com/better-auth/better-auth/issues/4250#issuecomment-3226530352) worked perfectly for me, thank you! > > I think this fix, while not perfect, It is important enough to be included in the documentation. ```ts import { betterAuth, BetterAuthOptions } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { bearer, customSession, getJwtToken, jwt, openAPI } from "better-auth/plugins"; import { authPrisma } from "@/lib/auth-prisma"; const options = { trustedOrigins: (process.env.BETTER_TRUSTED_ORIGINS ?? "").split(",").filter(Boolean), database: prismaAdapter(authPrisma, { provider: "sqlite" }), plugins: [openAPI(), bearer(), jwt()], emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: process.env.BETTER_GITHUB_CLIENT_ID as string, clientSecret: process.env.BETTER_GITHUB_CLIENT_SECRET as string, }, google: { enabled: true, clientId: process.env.BETTER_GOOGLE_CLIENT_ID as string, clientSecret: process.env.BETTER_GOOGLE_CLIENT_SECRET as string, }, }, advanced: { cookiePrefix: "vuer-auth", useSecureCookies: process.env.NODE_ENV === "production", }, } satisfies BetterAuthOptions; export const auth = betterAuth({ ...options, plugins: [ ...options.plugins, customSession( async ({ user, session }, ctx) => { console.log("================= customSession after ================="); console.log(ctx.context.returned); console.log(ctx.context.responseHeaders); const jwt = await getJwtToken(ctx, undefined); return { user, session: { ...session, jwt } }; }, options, { shouldMutateListDeviceSessionsEndpoint: true }, ), ], }) as ReturnType<typeof betterAuth<typeof options>>; ``` ``` TS2352: Conversion of type { handler: (request: Request) => Promise<Response>; api: import("/Users/garnet/projects/mewtwochips/node_modules/.pnpm/better-auth@1.3.11_next@15.4.7_@babel+core@7.28.4_react-dom@19.1.1_react@19.1.1__react@_e1647ea21c6ddce493c9a65686cc263e/node_modules/better-auth/dist/shared/better-auth.BBLxGH6k").h<{ ok: { <AsResp... to type { handler: (request: Request) => Promise<Response>; api: import("/Users/garnet/projects/mewtwochips/node_modules/.pnpm/better-auth@1.3.11_next@15.4.7_@babel+core@7.28.4_react-dom@19.1.1_react@19.1.1__react@_e1647ea21c6ddce493c9a65686cc263e/node_modules/better-auth/dist/shared/better-auth.BBLxGH6k").h<{ ok: { <AsResp... may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to unknown first. The types of options.plugins are incompatible between these types. Type '[{ id: "open-api"; endpoints: { generateOpenAPISchema: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({ body?: undefined; } & { method?: "GET" | undefined; } & { query?: Record<string, any> | undefined; } & ... 4 more ... & { ...; }) | undefined): Promise<...>; options: {...' is not comparable to type '[{ id: "open-api"; endpoints: { generateOpenAPISchema: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({ body?: undefined; } & { method?: "GET" | undefined; } & { query?: Record<string, any> | undefined; } & ... 4 more ... & { ...; }) | undefined): Promise<...>; options: {...'. Source has 4 element(s) but target allows only 3 ``` Even after doing this, I still encountered this error.
Author
Owner

@Pyakz commented on GitHub (Oct 26, 2025):

updates?

Image
<!-- gh-comment-id:3448034936 --> @Pyakz commented on GitHub (Oct 26, 2025): updates? <img width="1138" height="176" alt="Image" src="https://github.com/user-attachments/assets/be172dbf-204d-4e70-8190-80cbb7e556eb" />
Author
Owner

@Pyakz commented on GitHub (Oct 26, 2025):

I don’t want to add noise to this conversation, but it seems that only few users are encountering this error. I think it’s important to point out that I was working in a monorepo with pnpm and turbo. Based on the error messages I got, the type inference was conflicting between better-auth and the zod package.

how did you solve it? i am also using monorepo using turbo and bun

<!-- gh-comment-id:3448038048 --> @Pyakz commented on GitHub (Oct 26, 2025): > I don’t want to add noise to this conversation, but it seems that only few users are encountering this error. I think it’s important to point out that I was working in a `monorepo` with `pnpm` and `turbo`. Based on the error messages I got, the type inference was conflicting between `better-auth` and the `zod` package. how did you solve it? i am also using monorepo using turbo and bun
Author
Owner

@PI-Gorbo commented on GitHub (Nov 6, 2025):

After going back and forth and trying everything here, matching my zod version to the current version used by better-auth fixed my issue
I am in a monorepo setup using typescript references and yarn.

<!-- gh-comment-id:3496707159 --> @PI-Gorbo commented on GitHub (Nov 6, 2025): After going back and forth and trying everything here, matching my zod version to the current version used by better-auth fixed my issue I am in a monorepo setup using typescript references and yarn.
Author
Owner

@sanny-io commented on GitHub (Nov 6, 2025):

I solved this issue by directly installing into my project whatever packages appeared in the error messages, specifically:

  • @better-fetch/fetch
  • nanostores
  • undici-types

In your case @Pyakz, I would try directly installing @better-auth/core in your project.

<!-- gh-comment-id:3499553420 --> @sanny-io commented on GitHub (Nov 6, 2025): I solved this issue by directly installing into my project whatever packages appeared in the error messages, specifically: * `@better-fetch/fetch` * `nanostores` * `undici-types` In your case @Pyakz, I would try directly installing `@better-auth/core` in your project.
Author
Owner

@himself65 commented on GitHub (Nov 14, 2025):

We have fixed this in 1.4

<!-- gh-comment-id:3530435793 --> @himself65 commented on GitHub (Nov 14, 2025): We have fixed this in 1.4
Author
Owner

@pthieu commented on GitHub (Dec 2, 2025):

still not working on latest (1.4.4)

Image
<!-- gh-comment-id:3600187392 --> @pthieu commented on GitHub (Dec 2, 2025): still not working on latest (1.4.4) <img width="1418" height="426" alt="Image" src="https://github.com/user-attachments/assets/9a35fc36-9376-459c-bbd1-5fe4a583b3d1" />
Author
Owner

@Jordan-Hall commented on GitHub (Dec 2, 2025):

I had same issue on latest version

import type {} from 'better-auth';
import type {} from 'better-auth/adapters';
import type {} from '@better-auth/passkey';
import type {} from '@simplewebauthn/server';
import type {} from 'better-auth/plugins';

Had to do that in loads of places to resolve it

<!-- gh-comment-id:3601372161 --> @Jordan-Hall commented on GitHub (Dec 2, 2025): I had same issue on latest version ``` import type {} from 'better-auth'; import type {} from 'better-auth/adapters'; import type {} from '@better-auth/passkey'; import type {} from '@simplewebauthn/server'; import type {} from 'better-auth/plugins'; ``` Had to do that in loads of places to resolve it
Author
Owner

@mezotv commented on GitHub (Dec 9, 2025):

Still doesn't seem to be fixed in better-auth 1.4.6
Image

<!-- gh-comment-id:3633743135 --> @mezotv commented on GitHub (Dec 9, 2025): Still doesn't seem to be fixed in better-auth 1.4.6 <img width="1103" height="140" alt="Image" src="https://github.com/user-attachments/assets/0ec40500-3c23-403e-931f-f74ab1cfe866" />
Author
Owner

@Bekacru commented on GitHub (Dec 9, 2025):

seems like polar client type issue @mezotv - will check

<!-- gh-comment-id:3634000234 --> @Bekacru commented on GitHub (Dec 9, 2025): seems like polar client type issue @mezotv - will check
Author
Owner

@mezotv commented on GitHub (Dec 9, 2025):

seems like polar client type issue @mezotv - will check

Talked to polar and they said its an upstream thing https://github.com/polarsource/polar/issues/7956

<!-- gh-comment-id:3634005381 --> @mezotv commented on GitHub (Dec 9, 2025): > seems like polar client type issue @mezotv - will check Talked to polar and they said its an upstream thing https://github.com/polarsource/polar/issues/7956
Author
Owner

@Bekacru commented on GitHub (Dec 9, 2025):

the solution is exporting all types from polar but I'll see if it's incase from our side

<!-- gh-comment-id:3634019056 --> @Bekacru commented on GitHub (Dec 9, 2025): the solution is exporting all types from polar but I'll see if it's incase from our side
Author
Owner

@mezotv commented on GitHub (Dec 9, 2025):

the solution is exporting all types from polar but I'll see if it's incase from our side

Let me know if you find something. Here is the PR I used to test this https://github.com/usemarble/marble/pull/277

<!-- gh-comment-id:3634026566 --> @mezotv commented on GitHub (Dec 9, 2025): > the solution is exporting all types from polar but I'll see if it's incase from our side Let me know if you find something. Here is the PR I used to test this https://github.com/usemarble/marble/pull/277
Author
Owner

@Huh-David commented on GitHub (Mar 23, 2026):

I came across this issue updating to 1.5 and solved it like this:


const authClientPlugins = [
  adminClient({
    adminRoles: ADMIN_ROLES,
    roles: adminRoleMapping,
    ac: adminAccessControl,
  }),
  organizationClient({
    ac: orgAccessControl,
    roles: orgRoleMapping,
    teams: {
      enabled: true,
    },
  }),
  emailOTPClient(),
  // twoFactorClient(),
  customSessionClient<typeof auth>(),
] satisfies BetterAuthClientOptions['plugins'];

const authClientOptions = {
  baseURL: env.NEXT_PUBLIC_URL,
  plugins: authClientPlugins,
} satisfies BetterAuthClientOptions;
type CustomAuthClientOptions = typeof authClientOptions;

type AuthClientInstance = ReturnType<
  typeof createAuthClient<CustomAuthClientOptions>
>;

export const authClient: AuthClientInstance =
  createAuthClient<CustomAuthClientOptions>(authClientOptions);

Otherwise I had these errors:

lib/better-auth/auth-client.ts:25:14 - error TS2742: The inferred type of 'authClient' cannot be named without a reference to '@/node_modules/better-auth/dist/client/path-to-object.mjs'. This is likely not portable. A type annotation is necessary.

25 export const authClient = createAuthClient({
                ~~~~~~~~~~

lib/better-auth/auth-client.ts:25:14 - error TS2742: The inferred type of 'authClient' cannot be named without a reference to '@/node_modules/better-auth/dist/client/query.mjs'. This is likely not portable. A type annotation is necessary.

25 export const authClient = createAuthClient({
                ~~~~~~~~~~
<!-- gh-comment-id:4110531424 --> @Huh-David commented on GitHub (Mar 23, 2026): I came across this issue updating to 1.5 and solved it like this: ``` typescript const authClientPlugins = [ adminClient({ adminRoles: ADMIN_ROLES, roles: adminRoleMapping, ac: adminAccessControl, }), organizationClient({ ac: orgAccessControl, roles: orgRoleMapping, teams: { enabled: true, }, }), emailOTPClient(), // twoFactorClient(), customSessionClient<typeof auth>(), ] satisfies BetterAuthClientOptions['plugins']; const authClientOptions = { baseURL: env.NEXT_PUBLIC_URL, plugins: authClientPlugins, } satisfies BetterAuthClientOptions; type CustomAuthClientOptions = typeof authClientOptions; type AuthClientInstance = ReturnType< typeof createAuthClient<CustomAuthClientOptions> >; export const authClient: AuthClientInstance = createAuthClient<CustomAuthClientOptions>(authClientOptions); ``` Otherwise I had these errors: ``` shell lib/better-auth/auth-client.ts:25:14 - error TS2742: The inferred type of 'authClient' cannot be named without a reference to '@/node_modules/better-auth/dist/client/path-to-object.mjs'. This is likely not portable. A type annotation is necessary. 25 export const authClient = createAuthClient({ ~~~~~~~~~~ lib/better-auth/auth-client.ts:25:14 - error TS2742: The inferred type of 'authClient' cannot be named without a reference to '@/node_modules/better-auth/dist/client/query.mjs'. This is likely not portable. A type annotation is necessary. 25 export const authClient = createAuthClient({ ~~~~~~~~~~ ```
Author
Owner

@aurelientanguy commented on GitHub (Mar 24, 2026):

Found a temporary fix, it may help

<!-- gh-comment-id:4117609826 --> @aurelientanguy commented on GitHub (Mar 24, 2026): Found [a temporary fix](https://github.com/better-auth/better-auth/issues/8623#issuecomment-4117591146), it may help
Author
Owner

@github-actions[bot] commented on GitHub (Mar 31, 2026):

This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.

<!-- gh-comment-id:4165909025 --> @github-actions[bot] commented on GitHub (Mar 31, 2026): This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9872