[GH-ISSUE #8855] Admin plugin with custom ac/roles produces type incompatible with BetterAuthPlugin #11215

Closed
opened 2026-04-13 07:33:37 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @khanhduyvt0101 on GitHub (Mar 31, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8855

Originally assigned to: @ping-maxwell on GitHub.

Description

When using the admin() plugin with custom ac and roles (via createAccessControl), the returned plugin type is not assignable to BetterAuthPlugin. This prevents passing it in the plugins array without a type assertion.

The error is specifically about the use middleware type in the setRole endpoint — the middleware function type doesn't satisfy the Middleware interface.

Reproduction

import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins";
import { createAccessControl } from "better-auth/plugins/access";
import { defaultStatements } from "better-auth/plugins/admin/access";

const statement = {
  ...defaultStatements,
  blog: ["create", "read", "update", "delete", "publish"],
} as const;

const ac = createAccessControl(statement);

const adminRole = ac.newRole({
  user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"],
  session: ["list", "revoke", "delete"],
  blog: ["create", "read", "update", "delete", "publish"],
});

const writerRole = ac.newRole({
  blog: ["create", "read", "update", "delete", "publish"],
});

const userRole = ac.newRole({
  blog: [],
  user: [],
  session: [],
});

export const auth = betterAuth({
  database: { /* any adapter */ },
  plugins: [
    admin({
      ac,
      roles: {
        admin: adminRole,
        writer: writerRole,
        user: userRole,
      },
    }),
    // ^^^^^^ Error: Type '{ id: "admin"; ... }' is not assignable to type 'BetterAuthPlugin'.
  ],
});

Error

error TS2322: Type '{ id: "admin"; init(): ... }' is not assignable to type 'BetterAuthPlugin'.
  Types of property 'endpoints' are incompatible.
    Type '{ setRole: StrictEndpoint<"/admin/set-role", ...> ... }' is not assignable to type '{ [key: string]: Endpoint; }'.
      Property 'setRole' is incompatible with index signature.
        Type 'StrictEndpoint<...>' is not assignable to type 'Endpoint'.
          Types of property 'options' are incompatible.
            Types of property 'use' are incompatible.
              Type '((inputContext: MiddlewareInputContext<MiddlewareOptions>) => Promise<{ session: { user: UserWithRole; ... }; }>)[]' is not assignable to type 'Middleware[]'.
                Type '(...) => Promise<...>' is not assignable to type 'Middleware'.
                  Type '(...) => Promise<...>' is not assignable to type '{ options: Record<string, any>; }'.

Notes

  • Without ac/roles, admin() works fine — no type error.
  • The runtime works correctly — roles are properly enforced. Only TypeScript types are broken.
  • This is not the same as #6991 (custom role type inference), which was fixed in v1.4.10. That fix is about setRole/createUser accepting custom role strings. This issue is about the plugin itself not satisfying BetterAuthPlugin.

Workaround

plugins: [
  admin({ ac, roles: { ... } }) as ReturnType<typeof admin>,
],

This casts to the admin plugin's base return type, which satisfies BetterAuthPlugin. Safe because the runtime object IS the admin plugin.

Environment

  • better-auth: 1.5.6
  • typescript: 5.8.2
  • Node.js: 22.x
Originally created by @khanhduyvt0101 on GitHub (Mar 31, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8855 Originally assigned to: @ping-maxwell on GitHub. ## Description When using the `admin()` plugin with custom `ac` and `roles` (via `createAccessControl`), the returned plugin type is not assignable to `BetterAuthPlugin`. This prevents passing it in the `plugins` array without a type assertion. The error is specifically about the `use` middleware type in the `setRole` endpoint — the middleware function type doesn't satisfy the `Middleware` interface. ## Reproduction ```typescript import { betterAuth } from "better-auth"; import { admin } from "better-auth/plugins"; import { createAccessControl } from "better-auth/plugins/access"; import { defaultStatements } from "better-auth/plugins/admin/access"; const statement = { ...defaultStatements, blog: ["create", "read", "update", "delete", "publish"], } as const; const ac = createAccessControl(statement); const adminRole = ac.newRole({ user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"], session: ["list", "revoke", "delete"], blog: ["create", "read", "update", "delete", "publish"], }); const writerRole = ac.newRole({ blog: ["create", "read", "update", "delete", "publish"], }); const userRole = ac.newRole({ blog: [], user: [], session: [], }); export const auth = betterAuth({ database: { /* any adapter */ }, plugins: [ admin({ ac, roles: { admin: adminRole, writer: writerRole, user: userRole, }, }), // ^^^^^^ Error: Type '{ id: "admin"; ... }' is not assignable to type 'BetterAuthPlugin'. ], }); ``` ## Error ``` error TS2322: Type '{ id: "admin"; init(): ... }' is not assignable to type 'BetterAuthPlugin'. Types of property 'endpoints' are incompatible. Type '{ setRole: StrictEndpoint<"/admin/set-role", ...> ... }' is not assignable to type '{ [key: string]: Endpoint; }'. Property 'setRole' is incompatible with index signature. Type 'StrictEndpoint<...>' is not assignable to type 'Endpoint'. Types of property 'options' are incompatible. Types of property 'use' are incompatible. Type '((inputContext: MiddlewareInputContext<MiddlewareOptions>) => Promise<{ session: { user: UserWithRole; ... }; }>)[]' is not assignable to type 'Middleware[]'. Type '(...) => Promise<...>' is not assignable to type 'Middleware'. Type '(...) => Promise<...>' is not assignable to type '{ options: Record<string, any>; }'. ``` ## Notes - Without `ac`/`roles`, `admin()` works fine — no type error. - The runtime works correctly — roles are properly enforced. Only TypeScript types are broken. - This is **not** the same as #6991 (custom role type inference), which was fixed in v1.4.10. That fix is about `setRole`/`createUser` accepting custom role strings. This issue is about the plugin itself not satisfying `BetterAuthPlugin`. ## Workaround ```typescript plugins: [ admin({ ac, roles: { ... } }) as ReturnType<typeof admin>, ], ``` This casts to the admin plugin's base return type, which satisfies `BetterAuthPlugin`. Safe because the runtime object IS the admin plugin. ## Environment - `better-auth`: 1.5.6 - `typescript`: 5.8.2 - Node.js: 22.x
GiteaMirror added the bugorganization labels 2026-04-13 07:33:37 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 31, 2026):

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

This is a confirmed type-level regression introduced by the better-call v2 migration (PRs #8021 and #8466 in early March 2026). It's separate from #6991 as you noted.

Root cause: After better-call v2, createAuthEndpoint preserves the exact middleware tuple type in the Use generic parameter (e.g., [typeof adminMiddleware]) instead of widening it to any [1]. This makes the setRole endpoint's type structurally incompatible with the bare Endpoint type used in BetterAuthPlugin.endpoints [2]. When you add custom ac/roles, the generic O becomes more specific, which tightens the inferred types further and makes the incompatibility surface.

The framework itself already works around this internally with a double cast in getEndpoints [1], confirming this is a known structural limitation.

There's an open PR that should fix this: PR #8824 addresses ReadonlyArray and const generic issues in plugin type extraction [3]. This also affects other plugins — issue #8370 reports multiple plugins failing type-check in v1.5.3 for similar reasons [4].

Your workaround of casting to ReturnType&lt;typeof admin&gt; is reasonable in the meantime. An alternative that preserves more type safety is simply casting to as any just in the plugins array, since betterAuth() also uses the BetterAuthPluginRegistry interface (added in PR #7146) as an alternative type path that doesn't require direct structural assignability [5].

To reply, just mention @dosu.


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

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4161492900 --> @dosubot[bot] commented on GitHub (Mar 31, 2026): <!-- Greeting --> Hi @khanhduyvt0101! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a confirmed type-level regression introduced by the **better-call v2 migration** (PRs [#8021](https://github.com/better-auth/better-auth/pull/8021) and [#8466](https://github.com/better-auth/better-auth/pull/8466) in early March 2026). It's separate from #6991 as you noted. **Root cause:** After better-call v2, `createAuthEndpoint` preserves the exact middleware tuple type in the `Use` generic parameter (e.g., `[typeof adminMiddleware]`) instead of widening it to `any` [[1]](https://github.com/better-auth/better-auth/pull/8466). This makes the `setRole` endpoint's type structurally incompatible with the bare `Endpoint` type used in `BetterAuthPlugin.endpoints` [[2]](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/plugin.ts#L68-L72). When you add custom `ac`/`roles`, the generic `O` becomes more specific, which tightens the inferred types further and makes the incompatibility surface. The framework itself already works around this internally with a double cast in `getEndpoints` [[1]](https://github.com/better-auth/better-auth/pull/8466), confirming this is a known structural limitation. **There's an open PR that should fix this:** [PR #8824](https://github.com/better-auth/better-auth/pull/8824) addresses `ReadonlyArray` and `const` generic issues in plugin type extraction [[3]](https://github.com/better-auth/better-auth/pull/8824). This also affects other plugins — [issue #8370](https://github.com/better-auth/better-auth/issues/8370) reports multiple plugins failing type-check in v1.5.3 for similar reasons [[4]](https://github.com/better-auth/better-auth/issues/8370). Your workaround of casting to `ReturnType&lt;typeof admin&gt;` is reasonable in the meantime. An alternative that preserves more type safety is simply casting to `as any` just in the plugins array, since `betterAuth()` also uses the `BetterAuthPluginRegistry` interface (added in [PR #7146](https://github.com/better-auth/better-auth/pull/7146)) as an alternative type path that doesn't require direct structural assignability [[5]](https://github.com/better-auth/better-auth/pull/7146). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/82cac5ed-dc52-4077-a153-c9ddf023f3b2?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20confirmed%20type-level%20regression%20introduced%20by%20the%20%2A%2Abetter-call%20v2%20migration%2A%2A%20%28PRs%20%5B%238021%5D%28https%3A//github.com/better-auth/better-auth/pull/8021%29%20and%20%5B%238466%5D%28https%3A//github.com/better-auth/better-auth/pull/8466%29%20in%20early%20March%202026%29.%20It%27s%20separate%20from%20%236991%20as%20you%20noted.%0A%0A%2A%2ARoot%20cause%3A%2A%2A%20After%20better-call%20v2%2C%20%60createAuthEndpoint%60%20preserves%20the%20exact%20middleware%20tuple%20type%20in%20the%20%60Use%60%20generic%20parameter%20%28e.g.%2C%20%60%5Btypeof%20adminMiddleware%5D%60%29%20instead%20of%20widening%20it%20to%20%60any%60%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/8466%29.%20This%20makes%20the%20%60setRole%60%20endpoint%27s%20type%20structurally%20incompatible%20with%20the%20bare%20%60Endpoint%60%20type%20used%20in%20%60BetterAuthPlugin.endpoints%60%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/main/packages/core/src/types/plugin.ts%23L68-L72%29.%20When%20you%20add%20custom%20%60ac%60/%60roles%60%2C%20the%20generic%20%60O%60%20becomes%20more%20specific%2C%20which%20tightens%20the%20inferred%20types%20further%20and%20makes%20the%20incompatibility%20surface.%0A%0AThe%20framework%20itself%20already%20works%20around%20this%20internally%20with%20a%20double%20cast%20in%20%60getEndpoints%60%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/8466%29%2C%20confirming%20this%20is%20a%20known%20structural%20limitation.%0A%0A%2A%2AThere%27s%20an%20open%20PR%20that%20should%20fix%20this%3A%2A%2A%20%5BPR%20%238824%5D%28https%3A//github.com/better-auth/better-auth/pull/8824%29%20addresses%20%60ReadonlyArray%60%20and%20%60const%60%20generic%20issues%20in%20plugin%20type%20extraction%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/8824%29.%20This%20also%20affects%20other%20plugins%20%E2%80%94%20%5Bissue%20%238370%5D%28https%3A//github.com/better-auth/better-auth/issues/8370%29%20reports%20multiple%20plugins%20failing%20type-check%20in%20v1.5.3%20for%20similar%20reasons%20%5B%5B4%5D%5D%28https%3A//github.com/better-auth/better-auth/issues/8370%29.%0A%0AYour%20workaround%20of%20casting%20to%20%60ReturnType%26lt%3Btypeof%20admin%26gt%3B%60%20is%20reasonable%20in%20the%20meantime.%20An%20alternative%20that%20preserves%20more%20type%20safety%20is%20simply%20casting%20to%20%60as%20any%60%20just%20in%20the%20plugins%20array%2C%20since%20%60betterAuth%28%29%60%20also%20uses%20the%20%60BetterAuthPluginRegistry%60%20interface%20%28added%20in%20%5BPR%20%237146%5D%28https%3A//github.com/better-auth/better-auth/pull/7146%29%29%20as%20an%20alternative%20type%20path%20that%20doesn%27t%20require%20direct%20structural%20assignability%20%5B%5B5%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/7146%29.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/8855)
Author
Owner

@ping-maxwell commented on GitHub (Mar 31, 2026):

Works fine for me, could be related to tsconfig.
Could you share your tsconfig?

<!-- gh-comment-id:4162023140 --> @ping-maxwell commented on GitHub (Mar 31, 2026): Works fine for me, could be related to tsconfig. Could you share your tsconfig?
Author
Owner

@khanhduyvt0101 commented on GitHub (Apr 1, 2026):

Thanks for looking into this! Here's our tsconfig:

// tsconfig.base.json
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "module": "esnext",
    "target": "es2024",
    "lib": ["es2024"],
    "allowJs": false,
    "strict": true,
    "incremental": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "allowImportingTsExtensions": true,
    "importHelpers": true,
    "noEmit": true
  }
}
// apps/child/tsconfig.json
{
  "include": ["lib", "*.script.ts", "*.script.tsx"],
  "references": [],
  "extends": "../../tsconfig.base.json",
  "compilerOptions": { "jsx": "react-jsx" }
}

You're right — with skipLibCheck: true and the current pnpm-lock.yaml resolution, the error no longer reproduces.

The error was originally surfacing when we did a clean rm -rf node_modules && pnpm install and ran tsc without incremental cache. It's possible the dependency resolution changed between installs (we have better-auth@^1.5.6). The CI also reproduced it consistently on fresh installs.

We've removed the as ReturnType<typeof admin> cast and it now passes cleanly. If the error was caused by a transient dependency resolution, it may be worth noting that the type compatibility is fragile when using custom ac/roles with the admin plugin.

<!-- gh-comment-id:4167291371 --> @khanhduyvt0101 commented on GitHub (Apr 1, 2026): Thanks for looking into this! Here's our tsconfig: ```json // tsconfig.base.json { "compilerOptions": { "moduleResolution": "bundler", "module": "esnext", "target": "es2024", "lib": ["es2024"], "allowJs": false, "strict": true, "incremental": true, "verbatimModuleSyntax": true, "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true, "allowImportingTsExtensions": true, "importHelpers": true, "noEmit": true } } ``` ```json // apps/child/tsconfig.json { "include": ["lib", "*.script.ts", "*.script.tsx"], "references": [], "extends": "../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx" } } ``` You're right — with `skipLibCheck: true` and the current `pnpm-lock.yaml` resolution, the error no longer reproduces. The error was originally surfacing when we did a clean `rm -rf node_modules && pnpm install` and ran `tsc` without incremental cache. It's possible the dependency resolution changed between installs (we have `better-auth@^1.5.6`). The CI also reproduced it consistently on fresh installs. We've removed the `as ReturnType<typeof admin>` cast and it now passes cleanly. If the error was caused by a transient dependency resolution, it may be worth noting that the type compatibility is fragile when using custom `ac`/`roles` with the admin plugin.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11215