Plugins (magicLink, customSession, organization, etc.) fail type-check: endpoints incompatible with BetterAuthPlugin #2985

Open
opened 2026-03-13 10:32:30 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @Sw0xy on GitHub (Mar 4, 2026).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Clone the repo and install dependencies (e.g. pnpm install from monorepo root).
  2. From repo root or from packages/auth, run:
    pnpm exec eslint packages/auth/src/index.ts (or pnpm lint in packages/auth if it targets that file).
    Or run TypeScript check: pnpm exec tsc --noEmit -p packages/auth (or pnpm typecheck in packages/auth).
    3.The 8 TypeScript/type-check errors in packages/auth/src/index.ts appear (plugin assignability, betterAuth(config), and auth.$Infer.Organization / auth.$Infer.Member).

Current vs. Expected behavior

What happens

  1. In @acme/auth
    packages/auth/src/index.ts has 8 TypeScript errors: BetterAuth plugins (magicLink, customSession, organization, multiSession, admin) don’t match BetterAuthPlugin, so config doesn’t match BetterAuthOptions, and auth.$Infer.Organization / auth.$Infer.Member don’t exist on the inferred type. Lint and typecheck fail in this package.

  2. On the client side
    Apps that depend on @acme/auth (e.g. apps/web) cannot use the intended types. Exports such as Organization, Member, and possibly Session or other auth-related types are either missing, wrong, or not inferred correctly, so we can’t type components, API handlers, or tRPC procedures with the real auth types. The type errors in the auth package propagate: because the exports are broken, consumers don’t get usable types.

What we expected

  • pnpm lint and pnpm typecheck pass in packages/auth with no type errors.
  • In consuming apps we can import and use types from @acme/auth (e.g. Organization, Member, Session, Auth) and get correct autocomplete and type checking (e.g. auth.api.getSession() and organization/member fields typed correctly).

Summary

The bug is both build/lint failure in the auth package and unusable auth types on the client: the broken typings in packages/auth prevent consumers from getting the types they need.

What version of Better Auth are you using?

1.5.3

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:08:48 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T8132",
    "release": "25.2.0",
    "cpuCount": 10,
    "cpuModel": "Apple M4",
    "totalMemory": "16.00 GB",
    "freeMemory": "1.36 GB"
  },
  "node": {
    "version": "v25.2.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.1.6"
    },
    {
      "name": "react",
      "version": "catalog:react19"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "1.5.3",
    "config": null
  }
}

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

Backend, Client

Auth config (if applicable)

const config = {
  secret: process.env.BETTER_AUTH_SECRET,
  trustedOrigins: [baseUrl],
  advanced: {
    cookiePrefix: "asdf",
  },
  user: {
    additionalFields: {
      activePageId: {
        type: "string",
        required: false,
        defaultValue: null,
      },
      plan: {
        type: "string",
        required: false,
        defaultValue: "FREE",
      },
    },
    deleteUser: {
      enabled: true,
      sendDeleteAccountVerification: async ({ user, url }) => {
        await sendEmailViaResend({
          from,
          to: user.email,
          subject: "Delete your account",
          react: DeleteAccountEmail({
            userEmail: user.email,
            deleteAccountLink: url,
          }),
        });
      },
    },
  },
  database: prismaAdapter(userDb, {
    provider: "postgresql", // or "mysql", "postgresql", ...etc
  }),
  emailVerification: {
    async sendVerificationEmail({ user, url }) {
      await sendEmailViaResend({
        from,
        to: user.email,
        subject: "Verify your email address",
        html: `<a href="${url}">Verify your email address</a>`,
      });
    },
  },
  plugins: [
    magicLink({
      sendMagicLink: async ({ email, url, token }) => {
        await sendEmailViaResend({
          from,
          to: email,
          subject: "Verify your email address",
          react: MagicLinkEmail({
            userEmail: email,
            magicLink: url,
          }),
        });
      },
    }),
    customSession(async ({ user, session }) => {
      const data = await userDb.user.findUnique({
        where: {
          id: session.userId,
        },
        select: {
          plan: true,
          activePageId: true,
          role: true,
        },
      });

      return {
        user: {
          ...user,
          activePageId: data?.activePageId,
          plan: data?.plan,
          role: data?.role,
        },
        session,
      };
    }),
    organization({
      schema: {
        member: {
          additionalFields: {
            consistencyScore: {
              type: "number",
              required: false,
            },
            daysSinceLastConversion: {
              type: "number",
              required: false,
            },
            lastConversionAt: {
              type: "date",
              required: false,
            },
            returnOnAdSpend: {
              type: "number",
              required: false,
            },
            leadToConversionRate: {
              type: "number",
              required: false,
            },
            clickToConversionRate: {
              type: "number",
              required: false,
            },
            clickToLeadRate: {
              type: "number",
              required: false,
            },
            averageLifetimeValue: {
              type: "number",
              required: false,
            },
            earningsPerClick: {
              type: "number",
              required: false,
            },
            netRevenue: {
              type: "number",
              required: false,
            },
            totalCommissions: {
              type: "number",
              required: false,
            },
            totalSaleAmount: {
              type: "number",
              required: false,
            },
            totalSales: {
              type: "number",
              required: false,
            },
          },
        },
        organization: {
          additionalFields: {
            isSetupCompleted: {
              type: "boolean",
              defaultValue: false,
              required: false,
            },
            isActive: {
              type: "boolean",
              defaultValue: false,
              required: false,
            },
            activePageId: {
              type: "string",
              required: false,
            },
            minPayoutAmount: {
              type: "number",
              required: false,
            },
            termsUrl: {
              type: "string",
              required: true,
            },
            helpUrl: {
              type: "string",
              required: true,
            },
            supportEmail: {
              type: "string",
              required: true,
            },
          },
        },
      },
      async sendInvitationEmail({ email, id, inviter, organization }) {
        const invitationLink = `${process.env.NEXT_URL}/accept-invitation/${id}`;
        await sendEmailViaResend({
          subject: `${organization.name} Invitation`,
          from,
          variant: "primary",
          to: email,
          replyTo: ["support@acme.me", "acme@acme.me"],
          react: inviteMemberEmail({
            email: email,
            invitedByUsername: inviter.user.name,
            invitedByEmail: inviter.user.email,
            teamName: organization.name,
            inviteLink: invitationLink,
            organizationName: organization.name,
          }),
        });
      },
      organizationHooks: {
        afterCreateOrganization: async ({ organization }) => {
          await userDb.organization.update({
            where: {
              id: organization.id,
            },
            data: {
              isSetupCompleted: true,
            },
          });
        },
      },
      
      teams: {
        enabled: true,
        maximumTeams: 10, // Optional: limit teams per organization
        allowRemovingAllTeams: false, // Optional: prevent removing the last team
      },
      organizationLimit: 1,
    }),
    multiSession(),
    admin(),
    nextCookies(),
    apiKey([
      {
        configId: "user-keys",
        defaultPrefix: "user_",
        references: "user", // Default - owned by users
        defaultKeyLength: 32,
        rateLimit: {
          maxRequests: 50,
          timeWindow: 3 * 60 * 1000, // 3 minute
          enabled: true,
        },
        enableMetadata: true,
        permissions: {
          defaultPermissions: {
            ...
          },
        },
      },
      {
        configId: "org-keys",
        defaultPrefix: "org_",
        references: "organization", // Owned by organizations
        defaultKeyLength: 32,
        rateLimit: {
          maxRequests: 50,
          timeWindow: 3 * 60 * 1000, // 3 minute
          enabled: true,
        },
        enableMetadata: true,
        permissions: {
          defaultPermissions: {
            ...
          },
        },
      },
    ]),
  ],
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID ?? "",
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      redirectURI: `${baseUrl}/api/auth/callback/google`,
    },
  },
}

Additional context

Image Image
Originally created by @Sw0xy on GitHub (Mar 4, 2026). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Clone the repo and install dependencies (e.g. pnpm install from monorepo root). 2. From repo root or from packages/auth, run: pnpm exec eslint packages/auth/src/index.ts (or pnpm lint in packages/auth if it targets that file). Or run TypeScript check: pnpm exec tsc --noEmit -p packages/auth (or pnpm typecheck in packages/auth). 3.The 8 TypeScript/type-check errors in packages/auth/src/index.ts appear (plugin assignability, betterAuth(config), and auth.$Infer.Organization / auth.$Infer.Member). ### Current vs. Expected behavior **What happens** 1. **In `@acme/auth`** `packages/auth/src/index.ts` has 8 TypeScript errors: BetterAuth plugins (`magicLink`, `customSession`, `organization`, `multiSession`, `admin`) don’t match `BetterAuthPlugin`, so `config` doesn’t match `BetterAuthOptions`, and `auth.$Infer.Organization` / `auth.$Infer.Member` don’t exist on the inferred type. Lint and typecheck fail in this package. 2. **On the client side** Apps that depend on `@acme/auth` (e.g. `apps/web`) cannot use the intended types. Exports such as `Organization`, `Member`, and possibly `Session` or other auth-related types are either missing, wrong, or not inferred correctly, so we can’t type components, API handlers, or tRPC procedures with the real auth types. The type errors in the auth package propagate: because the exports are broken, consumers don’t get usable types. **What we expected** - `pnpm lint` and `pnpm typecheck` pass in `packages/auth` with no type errors. - In consuming apps we can import and use types from `@acme/auth` (e.g. `Organization`, `Member`, `Session`, `Auth`) and get correct autocomplete and type checking (e.g. `auth.api.getSession()` and organization/member fields typed correctly). **Summary** The bug is both **build/lint failure in the auth package** and **unusable auth types on the client**: the broken typings in `packages/auth` prevent consumers from getting the types they need. ### What version of Better Auth are you using? 1.5.3 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:08:48 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T8132", "release": "25.2.0", "cpuCount": 10, "cpuModel": "Apple M4", "totalMemory": "16.00 GB", "freeMemory": "1.36 GB" }, "node": { "version": "v25.2.1", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.2" }, "frameworks": [ { "name": "next", "version": "16.1.6" }, { "name": "react", "version": "catalog:react19" } ], "databases": null, "betterAuth": { "version": "1.5.3", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript const config = { secret: process.env.BETTER_AUTH_SECRET, trustedOrigins: [baseUrl], advanced: { cookiePrefix: "asdf", }, user: { additionalFields: { activePageId: { type: "string", required: false, defaultValue: null, }, plan: { type: "string", required: false, defaultValue: "FREE", }, }, deleteUser: { enabled: true, sendDeleteAccountVerification: async ({ user, url }) => { await sendEmailViaResend({ from, to: user.email, subject: "Delete your account", react: DeleteAccountEmail({ userEmail: user.email, deleteAccountLink: url, }), }); }, }, }, database: prismaAdapter(userDb, { provider: "postgresql", // or "mysql", "postgresql", ...etc }), emailVerification: { async sendVerificationEmail({ user, url }) { await sendEmailViaResend({ from, to: user.email, subject: "Verify your email address", html: `<a href="${url}">Verify your email address</a>`, }); }, }, plugins: [ magicLink({ sendMagicLink: async ({ email, url, token }) => { await sendEmailViaResend({ from, to: email, subject: "Verify your email address", react: MagicLinkEmail({ userEmail: email, magicLink: url, }), }); }, }), customSession(async ({ user, session }) => { const data = await userDb.user.findUnique({ where: { id: session.userId, }, select: { plan: true, activePageId: true, role: true, }, }); return { user: { ...user, activePageId: data?.activePageId, plan: data?.plan, role: data?.role, }, session, }; }), organization({ schema: { member: { additionalFields: { consistencyScore: { type: "number", required: false, }, daysSinceLastConversion: { type: "number", required: false, }, lastConversionAt: { type: "date", required: false, }, returnOnAdSpend: { type: "number", required: false, }, leadToConversionRate: { type: "number", required: false, }, clickToConversionRate: { type: "number", required: false, }, clickToLeadRate: { type: "number", required: false, }, averageLifetimeValue: { type: "number", required: false, }, earningsPerClick: { type: "number", required: false, }, netRevenue: { type: "number", required: false, }, totalCommissions: { type: "number", required: false, }, totalSaleAmount: { type: "number", required: false, }, totalSales: { type: "number", required: false, }, }, }, organization: { additionalFields: { isSetupCompleted: { type: "boolean", defaultValue: false, required: false, }, isActive: { type: "boolean", defaultValue: false, required: false, }, activePageId: { type: "string", required: false, }, minPayoutAmount: { type: "number", required: false, }, termsUrl: { type: "string", required: true, }, helpUrl: { type: "string", required: true, }, supportEmail: { type: "string", required: true, }, }, }, }, async sendInvitationEmail({ email, id, inviter, organization }) { const invitationLink = `${process.env.NEXT_URL}/accept-invitation/${id}`; await sendEmailViaResend({ subject: `${organization.name} Invitation`, from, variant: "primary", to: email, replyTo: ["support@acme.me", "acme@acme.me"], react: inviteMemberEmail({ email: email, invitedByUsername: inviter.user.name, invitedByEmail: inviter.user.email, teamName: organization.name, inviteLink: invitationLink, organizationName: organization.name, }), }); }, organizationHooks: { afterCreateOrganization: async ({ organization }) => { await userDb.organization.update({ where: { id: organization.id, }, data: { isSetupCompleted: true, }, }); }, }, teams: { enabled: true, maximumTeams: 10, // Optional: limit teams per organization allowRemovingAllTeams: false, // Optional: prevent removing the last team }, organizationLimit: 1, }), multiSession(), admin(), nextCookies(), apiKey([ { configId: "user-keys", defaultPrefix: "user_", references: "user", // Default - owned by users defaultKeyLength: 32, rateLimit: { maxRequests: 50, timeWindow: 3 * 60 * 1000, // 3 minute enabled: true, }, enableMetadata: true, permissions: { defaultPermissions: { ... }, }, }, { configId: "org-keys", defaultPrefix: "org_", references: "organization", // Owned by organizations defaultKeyLength: 32, rateLimit: { maxRequests: 50, timeWindow: 3 * 60 * 1000, // 3 minute enabled: true, }, enableMetadata: true, permissions: { defaultPermissions: { ... }, }, }, ]), ], socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID ?? "", clientSecret: process.env.GOOGLE_CLIENT_SECRET, redirectURI: `${baseUrl}/api/auth/callback/google`, }, }, } ``` ### Additional context <img width="710" height="1281" alt="Image" src="https://github.com/user-attachments/assets/e8c00a3a-5a6c-42cf-ad55-e96fe9fb1e7f" /> <img width="721" height="556" alt="Image" src="https://github.com/user-attachments/assets/b8872206-8cd1-4336-b986-c6e489402e4e" />
GiteaMirror added the need-more-informationbug labels 2026-03-13 10:32:30 -05:00
Author
Owner

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

Hi @Sw0xy,

We are not using ESLint and we don't have a packages/auth setup. Also, there are quite a lot of configurations involved, so it's hard to immediately identify the failure point. I tried some type tests but couldn't reproduce the issue right away.

Could you share a minimal reproducible repo with me? I'll check 🧐

Note

This might be related to customSession plugin setup.
https://better-auth.com/docs/concepts/session-management#caveats-on-customizing-session-response

@bytaesu commented on GitHub (Mar 9, 2026): Hi @Sw0xy, We are not using ESLint and we don't have a packages/auth setup. Also, there are quite a lot of configurations involved, so it's hard to immediately identify the failure point. I tried some type tests but couldn't reproduce the issue right away. Could you share a minimal reproducible repo with me? I'll check 🧐 > [!NOTE] > This might be related to `customSession` plugin setup. > https://better-auth.com/docs/concepts/session-management#caveats-on-customizing-session-response
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2985