How to handle types in Org API's with Dynamic Roles #1855

Closed
opened 2026-03-13 09:08:40 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @max-om on GitHub (Sep 5, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Enable dynamicAccessControl in organization plugin
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { ac } from "@/auth/permissions";
export const auth = betterAuth({
    plugins: [ 
        organization({ 
            ac, // Must be defined in order for dynamic access control to work
            dynamicAccessControl: { 
              enabled: true, 
            }, 
        }) 
    ] 
})
  1. Use any org API where role is a required prop in the body, for example createInvitation
try {
  const data = await auth.api.createInvitation({
    body: {
      email: body.email,
      role: body.role!,
      organizationId: activeOrganizationId!,
      resend: true,
    },
    headers: c.req.raw.headers,
  });

  logger.info(`Invitation sent to ${body.email} successfully.`);
  return c.json({
    message: `Invitation sent to ${body.email} successfully.`,
    data,
  });
} catch (error) {
  logger.error("Error sending invitation:", error);
  throw new Error(
    `Failed to send invitation to ${body.email}: ${JSON.stringify(error)}`
  );
}
  1. You'll see a type error like Type 'string' is not assignable to type '"member" | "owner" | "admin" | ("member" | "owner" | "admin")[]'
  2. If you add some default roles to the org plugin like
export const auth = betterAuth({
    plugins: [ 
        organization({ 
            ac, // Must be defined in order for dynamic access control to work
            dynamicAccessControl: { 
              enabled: true, 
            }, 
            roles: { owner, admin, staff, member },
        }) 
    ] 
})

I get the error

Type 'string' is not assignable to type '"member" | "staff" | "owner" | "admin" | ("member" | "staff" | "owner" | "admin")[]'.ts(2322)
index.d.ts(7120, 17): The expected type comes from property 'role' which is declared here on type '{ email: string; role: "member" | "staff" | "owner" | "admin" | ("member" | "staff" | "owner" | "admin")[]; organizationId?: string | undefined; resend?: boolean | undefined; }'

Current vs. Expected behavior

How should the types be handled with dynamic access control.

Image

What version of Better Auth are you using?

1.3.8

System info

Mac OS

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @max-om on GitHub (Sep 5, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Enable dynamicAccessControl in organization plugin ``` import { betterAuth } from "better-auth"; import { organization } from "better-auth/plugins"; import { ac } from "@/auth/permissions"; export const auth = betterAuth({ plugins: [ organization({ ac, // Must be defined in order for dynamic access control to work dynamicAccessControl: { enabled: true, }, }) ] }) ``` 2. Use any org API where role is a required prop in the body, for example `createInvitation` ``` try { const data = await auth.api.createInvitation({ body: { email: body.email, role: body.role!, organizationId: activeOrganizationId!, resend: true, }, headers: c.req.raw.headers, }); logger.info(`Invitation sent to ${body.email} successfully.`); return c.json({ message: `Invitation sent to ${body.email} successfully.`, data, }); } catch (error) { logger.error("Error sending invitation:", error); throw new Error( `Failed to send invitation to ${body.email}: ${JSON.stringify(error)}` ); } ``` 3. You'll see a type error like `Type 'string' is not assignable to type '"member" | "owner" | "admin" | ("member" | "owner" | "admin")[]'` 4. If you add some default roles to the org plugin like ``` export const auth = betterAuth({ plugins: [ organization({ ac, // Must be defined in order for dynamic access control to work dynamicAccessControl: { enabled: true, }, roles: { owner, admin, staff, member }, }) ] }) ``` I get the error ``` Type 'string' is not assignable to type '"member" | "staff" | "owner" | "admin" | ("member" | "staff" | "owner" | "admin")[]'.ts(2322) index.d.ts(7120, 17): The expected type comes from property 'role' which is declared here on type '{ email: string; role: "member" | "staff" | "owner" | "admin" | ("member" | "staff" | "owner" | "admin")[]; organizationId?: string | undefined; resend?: boolean | undefined; }' ``` ### Current vs. Expected behavior How should the types be handled with dynamic access control. <img width="1231" height="193" alt="Image" src="https://github.com/user-attachments/assets/6c999ac7-6d66-46e3-b41c-374391d0a39e" /> ### What version of Better Auth are you using? 1.3.8 ### System info ```bash Mac OS ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the plugin label 2026-03-13 09:08:40 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 5, 2025):

When you enable dynamicAccessControl in the organization plugin, roles can be created at runtime and are stored in the database. At runtime, the API (like createInvitation) expects the role property to be a string or string[], supporting dynamic roles. However, TypeScript's type inference remains static: if you define static roles in your config, the type for role becomes a union of those literals, and passing a plain string (like body.role) causes a type error because TypeScript can't guarantee it's one of the allowed values.

If you want to support dynamic roles, the best practice is to rely on the dynamic typing: don't pass a static roles object to the plugin config, and TypeScript will infer role: string | string[] for the API, which matches what you want for dynamic roles. If you do need to define some static roles for intellisense or autocomplete, but still want to allow dynamic roles, you can use a type assertion or cast when calling the API:

await auth.api.createInvitation({
  body: {
    email: body.email,
    role: body.role as string, // or as string[]
    organizationId: activeOrganizationId!,
    resend: true,
  },
  headers: c.req.raw.headers,
});

This tells TypeScript to accept any string, matching the runtime behavior. You can also broaden the type of body.role in your own code to string or string[] if needed.

For more details, see the official documentation on dynamic access control, which confirms that with dynamic access control enabled, the API expects role: string | string[] and supports dynamic roles at runtime.

If you need to check permissions for dynamic roles on the client, use the hasPermission API instead of checkRolePermission, since the latter does not include dynamic roles due to its synchronous nature on the client side.

Recent updates also removed limits on the number of roles and always normalize role names to lowercase internally, making dynamic role management more flexible (PR 4337).

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Sep 5, 2025): <!-- Answer --> When you enable `dynamicAccessControl` in the organization plugin, roles can be created at runtime and are stored in the database. At runtime, the API (like `createInvitation`) expects the `role` property to be a `string` or `string[]`, supporting dynamic roles. However, TypeScript's type inference remains static: if you define static roles in your config, the type for `role` becomes a union of those literals, and passing a plain `string` (like `body.role`) causes a type error because TypeScript can't guarantee it's one of the allowed values. If you want to support dynamic roles, the best practice is to rely on the dynamic typing: don't pass a static `roles` object to the plugin config, and TypeScript will infer `role: string | string[]` for the API, which matches what you want for dynamic roles. If you do need to define some static roles for intellisense or autocomplete, but still want to allow dynamic roles, you can use a type assertion or cast when calling the API: ```ts await auth.api.createInvitation({ body: { email: body.email, role: body.role as string, // or as string[] organizationId: activeOrganizationId!, resend: true, }, headers: c.req.raw.headers, }); ``` This tells TypeScript to accept any string, matching the runtime behavior. You can also broaden the type of `body.role` in your own code to `string` or `string[]` if needed. For more details, see the [official documentation on dynamic access control](https://github.com/better-auth/better-auth/blob/e812142c6cb91a65bba022b77f1176f7cca6dff2/docs/content/docs/plugins/organization.mdx), which confirms that with dynamic access control enabled, the API expects `role: string | string[]` and supports dynamic roles at runtime. If you need to check permissions for dynamic roles on the client, use the `hasPermission` API instead of `checkRolePermission`, since the latter does not include dynamic roles due to its synchronous nature on the client side. Recent updates also removed limits on the number of roles and always normalize role names to lowercase internally, making dynamic role management more flexible ([PR 4337](https://github.com/better-auth/better-auth/pull/4337)). <!-- 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/fc12918b-8abe-4a06-bd9b-45803b07a478?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fc12918b-8abe-4a06-bd9b-45803b07a478?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fc12918b-8abe-4a06-bd9b-45803b07a478?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fc12918b-8abe-4a06-bd9b-45803b07a478?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fc12918b-8abe-4a06-bd9b-45803b07a478?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fc12918b-8abe-4a06-bd9b-45803b07a478?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fc12918b-8abe-4a06-bd9b-45803b07a478?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/4463)
Author
Owner

@max-om commented on GitHub (Sep 5, 2025):

@dosu only type assertion of any works

@max-om commented on GitHub (Sep 5, 2025): @dosu only type assertion of `any` works
Author
Owner

@dosubot[bot] commented on GitHub (Sep 5, 2025):

This is a known limitation with TypeScript's static type system and Better Auth's dynamic role support. When you enable dynamicAccessControl, roles are stored and managed at runtime, but TypeScript can only infer types from what's statically defined in your config. If you provide static roles, the API expects a union of those literals; if you want truly dynamic roles, you have to cast to any or string to bypass the type check—there's no safer or more idiomatic workaround in the codebase or docs right now. Internally, Better Auth also uses type assertions like role as string for dynamic roles, and runtime validation is handled by Zod schemas, not TypeScript types (source).

If you want to minimize risk, you can validate the role value at runtime before passing it to the API, but for type safety, casting is currently required when using dynamic roles (source).

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Sep 5, 2025): <!-- Answer --> This is a known limitation with TypeScript's static type system and Better Auth's dynamic role support. When you enable dynamicAccessControl, roles are stored and managed at runtime, but TypeScript can only infer types from what's statically defined in your config. If you provide static roles, the API expects a union of those literals; if you want truly dynamic roles, you have to cast to `any` or `string` to bypass the type check—there's no safer or more idiomatic workaround in the codebase or docs right now. Internally, Better Auth also uses type assertions like `role as string` for dynamic roles, and runtime validation is handled by Zod schemas, not TypeScript types ([source](https://github.com/better-auth/better-auth/blob/e812142c6cb91a65bba022b77f1176f7cca6dff2/packages/better-auth/src/plugins/organization/routes/crud-invites.ts)). If you want to minimize risk, you can validate the role value at runtime before passing it to the API, but for type safety, casting is currently required when using dynamic roles ([source](https://github.com/better-auth/better-auth/blob/e812142c6cb91a65bba022b77f1176f7cca6dff2/packages/better-auth/src/plugins/organization/schema.ts)). <!-- 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/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/b6bbfba2-ca09-4f25-8df8-73c2f219a9cd?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/4463)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1855