[GH-ISSUE #2687] hasPermission doesn't work client-side #9303

Closed
opened 2026-04-13 04:43:57 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @mintydev789 on GitHub (May 17, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2687

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I am following the documentation for checking if the user has a given permission, but the promise returns a 401 no matter what (I have tried logging in with an admin account, but that still results in canAccessModDashboard returning 401). This is the setup I have:

import { createAuthClient } from "better-auth/react";
import { adminClient, inferAdditionalFields, usernameClient } from "better-auth/client/plugins";
import { auth } from "~/server/auth.ts";
import { ac, admin } from "~/server/permissions.ts";

export const authClient = createAuthClient({
  plugins: [
    usernameClient(),
    adminClient({
      ac,
      roles: {
        admin,
      },
    }),
    inferAdditionalFields<typeof auth>(),
  ],
});

export const canAccessModDashboard = await authClient.admin.hasPermission({ permissions: { modDashboard: ["view"] } });

It's also possible that it works correctly, but I'm doing something wrong due to the documentation being somewhat unclear. For completeness, here is also my permissions.ts setup:

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

const statement = {
  ...defaultStatements,
  modDashboard: ["view", "view-stats"],
  competitions: ["create", "update", "approve", "delete"],
  meetups: ["create", "update", "approve", "delete"],
  persons: ["create", "update", "approve", "delete"],
} as const;

export const ac = createAccessControl(statement);

export const admin = ac.newRole({
  ...adminAc.statements,
  modDashboard: [...statement.modDashboard],
  competitions: [...statement.competitions],
  meetups: [...statement.meetups],
  persons: [...statement.persons],
});

Current vs. Expected behavior

The function always returns a 401. It should not do so when the user has the specified permissions.

What version of Better Auth are you using?

1.2.7

Provide environment information

Arco Linux. Deno runtime.

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

Client

Auth config (if applicable)

import "server-only";
import { betterAuth } from "better-auth";
import * as bcrypt from "bcrypt";
import { db } from "./db/provider.ts";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { admin as adminPlugin, username } from "better-auth/plugins";
import { accounts, sessions, users, verifications } from "~/server/db/schema/auth-schema.ts";
import { C } from "~/helpers/constants.ts";
import { sendResetPassword, sendVerificationCode } from "~/server/mailer.ts";
import { ac, admin } from "~/server/permissions.ts";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      user: users,
      session: sessions,
      account: accounts,
      verification: verifications,
    },
  }),
  plugins: [
    nextCookies(),
    username(),
    adminPlugin({
      ac,
      roles: {
        admin,
      },
    }),
  ],
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    sendResetPassword: ({ user, url }) => sendResetPassword(user.email, url),
    password: {
      hash: (password: string) => bcrypt.hash(password, C.passwordSaltRounds),
      verify: (data: { hash: string; password: string }) => bcrypt.compare(data.password, data.hash),
    },
  },
  emailVerification: {
    sendVerificationEmail: ({ user, url }) => sendVerificationCode(user.email, url),
  },
  user: {
    additionalFields: {
      username: {
        type: "string",
        required: true,
      },
      personId: {
        type: "number",
        required: false,
      },
    },
    deleteUser: {
      enabled: true,
    },
  },
});

Additional context

No response

Originally created by @mintydev789 on GitHub (May 17, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2687 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I am following the [documentation](https://better-auth.vercel.app/docs/plugins/admin#access-control-usage) for checking if the user has a given permission, but the promise returns a 401 no matter what (I have tried logging in with an admin account, but that still results in `canAccessModDashboard` returning 401). This is the setup I have: ```ts import { createAuthClient } from "better-auth/react"; import { adminClient, inferAdditionalFields, usernameClient } from "better-auth/client/plugins"; import { auth } from "~/server/auth.ts"; import { ac, admin } from "~/server/permissions.ts"; export const authClient = createAuthClient({ plugins: [ usernameClient(), adminClient({ ac, roles: { admin, }, }), inferAdditionalFields<typeof auth>(), ], }); export const canAccessModDashboard = await authClient.admin.hasPermission({ permissions: { modDashboard: ["view"] } }); ``` It's also possible that it works correctly, but I'm doing something wrong due to the documentation being somewhat unclear. For completeness, here is also my `permissions.ts` setup: ```ts import { createAccessControl } from "better-auth/plugins/access"; import { adminAc, defaultStatements } from "better-auth/plugins/admin/access"; const statement = { ...defaultStatements, modDashboard: ["view", "view-stats"], competitions: ["create", "update", "approve", "delete"], meetups: ["create", "update", "approve", "delete"], persons: ["create", "update", "approve", "delete"], } as const; export const ac = createAccessControl(statement); export const admin = ac.newRole({ ...adminAc.statements, modDashboard: [...statement.modDashboard], competitions: [...statement.competitions], meetups: [...statement.meetups], persons: [...statement.persons], }); ``` ### Current vs. Expected behavior The function always returns a 401. It should not do so when the user has the specified permissions. ### What version of Better Auth are you using? 1.2.7 ### Provide environment information ```bash Arco Linux. Deno runtime. ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import "server-only"; import { betterAuth } from "better-auth"; import * as bcrypt from "bcrypt"; import { db } from "./db/provider.ts"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { nextCookies } from "better-auth/next-js"; import { admin as adminPlugin, username } from "better-auth/plugins"; import { accounts, sessions, users, verifications } from "~/server/db/schema/auth-schema.ts"; import { C } from "~/helpers/constants.ts"; import { sendResetPassword, sendVerificationCode } from "~/server/mailer.ts"; import { ac, admin } from "~/server/permissions.ts"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: { user: users, session: sessions, account: accounts, verification: verifications, }, }), plugins: [ nextCookies(), username(), adminPlugin({ ac, roles: { admin, }, }), ], emailAndPassword: { enabled: true, requireEmailVerification: true, sendResetPassword: ({ user, url }) => sendResetPassword(user.email, url), password: { hash: (password: string) => bcrypt.hash(password, C.passwordSaltRounds), verify: (data: { hash: string; password: string }) => bcrypt.compare(data.password, data.hash), }, }, emailVerification: { sendVerificationEmail: ({ user, url }) => sendVerificationCode(user.email, url), }, user: { additionalFields: { username: { type: "string", required: true, }, personId: { type: "number", required: false, }, }, deleteUser: { enabled: true, }, }, }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 04:43:57 -05:00
Author
Owner

@mintydev789 commented on GitHub (May 17, 2025):

I should also mention I consume the output of the function directly in my client-side React code using canAccessModDashboard.data. I end up having a hydration error (seemingly because it's actually returning correctly while React does the initial render server-side (I'm using Next JS).

<!-- gh-comment-id:2888367991 --> @mintydev789 commented on GitHub (May 17, 2025): I should also mention I consume the output of the function directly in my client-side React code using `canAccessModDashboard.data`. I end up having a hydration error (seemingly because it's actually returning correctly while React does the initial render server-side (I'm using Next JS).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9303