[GH-ISSUE #8823] fix(types): server auth.api loses plugin endpoint types in lazy singleton / factory patterns #11202

Open
opened 2026-04-13 07:33:03 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @jmalmo on GitHub (Mar 29, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8823

Describe the bug

When using betterAuth() inside a factory function (lazy singleton pattern), auth.api loses TypeScript types for plugin-contributed endpoints. For example, auth.api.createUser() from the admin() plugin works at runtime but TypeScript reports:

Property 'createUser' does not exist on type 'InferAPI<...>'

This forces (auth.api as any).createUser(...) workarounds, losing type safety for all admin plugin methods (createUser, removeUser, listUsers, setRole, banUser, etc.).

Affected area(s)

Types

To Reproduce

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

// Common pattern: lazy singleton for Nitro SSR / serverless
function createAuth() {
  return betterAuth({
    database: "...",
    plugins: [admin()],
  });
}

type Auth = ReturnType<typeof createAuth>;

// ❌ TypeScript error — but works at runtime
declare const auth: Auth;
auth.api.createUser({ body: { email: "test@test.com", password: "test", name: "Test", role: "user" } });
//        ^^^^^^^^^^  Property 'createUser' does not exist on type 'InferAPI<...>'

The same issue occurs when:

  • Using ReturnType<typeof betterAuth> as a type annotation
  • Storing options in a variable before passing to betterAuth()
  • Combining plugins with different shapes (e.g., [admin(), tanstackStartCookies()])

Expected behavior

auth.api.createUser, auth.api.removeUser, auth.api.listUsers, etc. should be properly typed regardless of whether betterAuth() is called inline or inside a factory function.

Root cause

The PluginEndpoint type in getEndpoints() uses Array<infer T> which doesn't match ReadonlyArray. When betterAuth() uses a const generic or the caller stores options with as const, the plugin array becomes readonly, and endpoint type extraction silently fails (falls through to {}).

Additionally, all plugin type extraction utilities (InferPluginTypes, InferPluginErrorCodes, InferPluginIDs, InferPluginContext, InferDBFieldsFromPlugins/Input) only handle Array<infer T>, not ReadonlyArray<infer T>.

Better Auth Version

1.5.6 (also affects earlier versions — same type definitions)

Additional context

  • Related: #3015 (same symptom, closed without resolution)
  • Related: #7043 (client-side variant)
  • PR with fix: coming
Originally created by @jmalmo on GitHub (Mar 29, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8823 ### Describe the bug When using `betterAuth()` inside a factory function (lazy singleton pattern), `auth.api` loses TypeScript types for plugin-contributed endpoints. For example, `auth.api.createUser()` from the `admin()` plugin works at runtime but TypeScript reports: ``` Property 'createUser' does not exist on type 'InferAPI<...>' ``` This forces `(auth.api as any).createUser(...)` workarounds, losing type safety for all admin plugin methods (`createUser`, `removeUser`, `listUsers`, `setRole`, `banUser`, etc.). ### Affected area(s) Types ### To Reproduce ```typescript import { betterAuth } from "better-auth"; import { admin } from "better-auth/plugins"; // Common pattern: lazy singleton for Nitro SSR / serverless function createAuth() { return betterAuth({ database: "...", plugins: [admin()], }); } type Auth = ReturnType<typeof createAuth>; // ❌ TypeScript error — but works at runtime declare const auth: Auth; auth.api.createUser({ body: { email: "test@test.com", password: "test", name: "Test", role: "user" } }); // ^^^^^^^^^^ Property 'createUser' does not exist on type 'InferAPI<...>' ``` The same issue occurs when: - Using `ReturnType<typeof betterAuth>` as a type annotation - Storing options in a variable before passing to `betterAuth()` - Combining plugins with different shapes (e.g., `[admin(), tanstackStartCookies()]`) ### Expected behavior `auth.api.createUser`, `auth.api.removeUser`, `auth.api.listUsers`, etc. should be properly typed regardless of whether `betterAuth()` is called inline or inside a factory function. ### Root cause The `PluginEndpoint` type in `getEndpoints()` uses `Array<infer T>` which doesn't match `ReadonlyArray`. When `betterAuth()` uses a `const` generic or the caller stores options with `as const`, the plugin array becomes `readonly`, and endpoint type extraction silently fails (falls through to `{}`). Additionally, all plugin type extraction utilities (`InferPluginTypes`, `InferPluginErrorCodes`, `InferPluginIDs`, `InferPluginContext`, `InferDBFieldsFromPlugins/Input`) only handle `Array<infer T>`, not `ReadonlyArray<infer T>`. ### Better Auth Version 1.5.6 (also affects earlier versions — same type definitions) ### Additional context - Related: #3015 (same symptom, closed without resolution) - Related: #7043 (client-side variant) - PR with fix: coming
GiteaMirror added the bug label 2026-04-13 07:33:03 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11202