Custom Permissions Not Working
Hey there
Since 2 updates ago i seem to get a type-error on custom permissions i wonder if anything has changed without updating the docs?
Thanks in advance



15 Replies
can you share your whole auth config?
also your tsconfig
Hi i certainly can the auth config is rather large though i got some cleaning up todo:
Auth.ts (part 1):
tsConfig:
permissions.ts:
import { betterAuth } from "better-auth";
import { organization, twoFactor } from "better-auth/plugins";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
import { sendMail } from "./send-mail";
import { render } from "@react-email/components";
import { reactInvitationEmail } from "./email/invitation-email";
import { ac, member, admin, machinist, conducteur, ploegbaas, truckdriver, groundworker, paver } from "./permissions/permissions";
import { reactResetPasswordEmail } from "./email/resetpassword-email";
import { reactOtpEmail } from "./email/otp-email";
import { reactEmailVerification } from "./email/verification-email";
const prisma = new PrismaClient();
export const auth = betterAuth({
appName: "redacted",
trustedOrigins: ["http://localhost:3000", "https://www.redacted.be", "https://redacted.be"],
database: prismaAdapter(prisma, {
provider: "mysql",
}),
advanced: {
cookiePrefix: "parapluu",
crossSubDomainCookies: {
enabled: true
}
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
async sendResetPassword({ user, url }) {
try {
await sendMail({
email: "[email protected]",
sendTo: user.email,
subject: "Wachtwoord reset",
text: "",
html: await render(
reactResetPasswordEmail({
username: user.name,
resetLink: url
})
)
});
} catch (error) {
console.error("Error sending reset password email:", error);
}
},
},
import { betterAuth } from "better-auth";
import { organization, twoFactor } from "better-auth/plugins";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
import { sendMail } from "./send-mail";
import { render } from "@react-email/components";
import { reactInvitationEmail } from "./email/invitation-email";
import { ac, member, admin, machinist, conducteur, ploegbaas, truckdriver, groundworker, paver } from "./permissions/permissions";
import { reactResetPasswordEmail } from "./email/resetpassword-email";
import { reactOtpEmail } from "./email/otp-email";
import { reactEmailVerification } from "./email/verification-email";
const prisma = new PrismaClient();
export const auth = betterAuth({
appName: "redacted",
trustedOrigins: ["http://localhost:3000", "https://www.redacted.be", "https://redacted.be"],
database: prismaAdapter(prisma, {
provider: "mysql",
}),
advanced: {
cookiePrefix: "parapluu",
crossSubDomainCookies: {
enabled: true
}
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
async sendResetPassword({ user, url }) {
try {
await sendMail({
email: "[email protected]",
sendTo: user.email,
subject: "Wachtwoord reset",
text: "",
html: await render(
reactResetPasswordEmail({
username: user.name,
resetLink: url
})
)
});
} catch (error) {
console.error("Error sending reset password email:", error);
}
},
},
emailVerification: {
autoSignInAfterVerification: true,
sendVerificationEmail: async ( { user, url } ) => {
try {
const verLink = url.replace(/callbackURL=\/$/, "callbackURL=/login");
await sendMail({
email: "[email protected]",
sendTo: user.email,
subject: "Account verificatie",
text: "",
html: await render(
reactEmailVerification({
username: user.name,
verificationLink: verLink
})
)
});
console.log("Verification email sent successfully.");
} catch (error) {
console.error("Error sending verification email:", error);
}
}
},
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }) {
try {
await sendMail({
email: "[email protected]",
sendTo: user.email,
subject: "OTP Code",
text: "",
html: await render(
reactOtpEmail({
username: user.name,
otpCode: otp
})
)
});
} catch (error) {
console.error("Error sending OTP email:", error);
}
}
}
}),
organization({
ac: ac,
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
async sendInvitationEmail(data) {
try {
const inviteLink = `https://redacted.be/accept-invitation/${data.id}`;
await sendMail({
email: "[email protected]",
sendTo: data.email,
subject: `Invitatie redacted${data.organization.name}`,
text: "",
html: await render(
reactInvitationEmail({
username: data.email,
invitedByUsername: data.inviter.user.name,
invitedByEmail: data.inviter.user.email,
teamName: data.organization.name,
inviteLink: inviteLink,
teamImage: data.organization.logo || "",
})
)
});
} catch (error) {
console.error("Error sending invitation email:", error);
}
},
})
]
});
emailVerification: {
autoSignInAfterVerification: true,
sendVerificationEmail: async ( { user, url } ) => {
try {
const verLink = url.replace(/callbackURL=\/$/, "callbackURL=/login");
await sendMail({
email: "[email protected]",
sendTo: user.email,
subject: "Account verificatie",
text: "",
html: await render(
reactEmailVerification({
username: user.name,
verificationLink: verLink
})
)
});
console.log("Verification email sent successfully.");
} catch (error) {
console.error("Error sending verification email:", error);
}
}
},
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }) {
try {
await sendMail({
email: "[email protected]",
sendTo: user.email,
subject: "OTP Code",
text: "",
html: await render(
reactOtpEmail({
username: user.name,
otpCode: otp
})
)
});
} catch (error) {
console.error("Error sending OTP email:", error);
}
}
}
}),
organization({
ac: ac,
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
async sendInvitationEmail(data) {
try {
const inviteLink = `https://redacted.be/accept-invitation/${data.id}`;
await sendMail({
email: "[email protected]",
sendTo: data.email,
subject: `Invitatie redacted${data.organization.name}`,
text: "",
html: await render(
reactInvitationEmail({
username: data.email,
invitedByUsername: data.inviter.user.name,
invitedByEmail: data.inviter.user.email,
teamName: data.organization.name,
inviteLink: inviteLink,
teamImage: data.organization.logo || "",
})
)
});
} catch (error) {
console.error("Error sending invitation email:", error);
}
},
})
]
});
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
import { createAccessControl } from "better-auth/plugins/access";
const statement = {
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
} as const;
export const ac = createAccessControl(statement);
export const member = ac.newRole({});
export const admin = ac.newRole({
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
});
/* Infra Roles */
export const machinist = ac.newRole({
crewInvite: ["view"],
crew: ["view"],
crewInventory: ["view"],
});
export const conducteur = ac.newRole({
crewInventory: ["view"],
});
export const ploegbaas = ac.newRole({
crewInventory: ["view"],
});
export const truckdriver = ac.newRole({
crewInventory: ["view"],
});
export const groundworker = ac.newRole({
crewInventory: ["view"],
});
export const paver = ac.newRole({
crewInventory: ["view"],
});
/* End infra roles */
import { createAccessControl } from "better-auth/plugins/access";
const statement = {
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
} as const;
export const ac = createAccessControl(statement);
export const member = ac.newRole({});
export const admin = ac.newRole({
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
});
/* Infra Roles */
export const machinist = ac.newRole({
crewInvite: ["view"],
crew: ["view"],
crewInventory: ["view"],
});
export const conducteur = ac.newRole({
crewInventory: ["view"],
});
export const ploegbaas = ac.newRole({
crewInventory: ["view"],
});
export const truckdriver = ac.newRole({
crewInventory: ["view"],
});
export const groundworker = ac.newRole({
crewInventory: ["view"],
});
export const paver = ac.newRole({
crewInventory: ["view"],
});
/* End infra roles */
could you try to add
"baseUrl": "."
in your tsconfig?This did not solve it i reran typescript check and got 11 error's it used to work before so thats why i am kinda like huh:
It seems like the role's are not getting set
it's like this:
now requires something like
node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:4867:21
4867 role: InferRolesFromOption<O>;
~~~~
The expected type comes from property 'role' which is declared here on type 'Prettify<{ email: string; role: "admin" | "member" | "owner"; organizationId?: string | undefined; resend?: boolean | undefined; } & { fetchOptions?: { cache?: RequestCache | undefined; ... 34 more ...; disableValidation?: boolean | undefined; } | undefined; }>'
node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:6555:21
6555 permission: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
~~~~~~~~~~
The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("create" | "update" | "delete")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("create" | ... 1 more ... | "delete")[] | undefined; }; organizationId?: string | undefin...'
node_modules/better-auth/dist/plugins/access/index.d.ts:39:5
39 statements: TStatements;
~~~~~~~~~~
'statements' is declared here.
lib/auth.ts:103:17 - error TS2741: Property 'authorize' is missing in type 'Role<{ readonly crewInvite: readonly ["view"]; readonly crew: readonly ["create", "update", "edit", "delete", "view"]; readonly crewInventory: readonly ["view"]; readonly invitePerms: readonly ["create"]; }>' but required in type 'Role<any>'.
103 paver
node_modules/better-auth/dist/plugins/access/index.d.ts:13:5
13 authorize: (request: any, connector?: "OR" | "AND") => AuthortizeResponse;
~~~~~~~~~
'authorize' is declared here.
node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:4867:21
4867 role: InferRolesFromOption<O>;
~~~~
The expected type comes from property 'role' which is declared here on type 'Prettify<{ email: string; role: "admin" | "member" | "owner"; organizationId?: string | undefined; resend?: boolean | undefined; } & { fetchOptions?: { cache?: RequestCache | undefined; ... 34 more ...; disableValidation?: boolean | undefined; } | undefined; }>'
node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:6555:21
6555 permission: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
~~~~~~~~~~
The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("create" | "update" | "delete")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("create" | ... 1 more ... | "delete")[] | undefined; }; organizationId?: string | undefin...'
node_modules/better-auth/dist/plugins/access/index.d.ts:39:5
39 statements: TStatements;
~~~~~~~~~~
'statements' is declared here.
lib/auth.ts:103:17 - error TS2741: Property 'authorize' is missing in type 'Role<{ readonly crewInvite: readonly ["view"]; readonly crew: readonly ["create", "update", "edit", "delete", "view"]; readonly crewInventory: readonly ["view"]; readonly invitePerms: readonly ["create"]; }>' but required in type 'Role<any>'.
103 paver
node_modules/better-auth/dist/plugins/access/index.d.ts:13:5
13 authorize: (request: any, connector?: "OR" | "AND") => AuthortizeResponse;
~~~~~~~~~
'authorize' is declared here.
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
admin: {
statements: statements,
authorize(request, connector) {
},
}
admin: {
statements: statements,
authorize(request, connector) {
},
}
as you can see that whole part is red

It could be because you have 2 versions of better auth. The new release adds new interface to roles and that's what causing the type mismatch. Check your better-auth versions if you have multiple, remove node_modules and re-install.
Alright let me try that
That was indeed the issue for some reason it reffered to a global installed better-auth even though i don't remember globally installing it thanks!
However the custom permission still seems to error after loading
the standard (invitation,team) etc works but my crewInventory still gives:
this is the list of packages
Also it's not only local its local and on the production server
Ima try to revert to a older version and see if it works
Reverting to 1.1.19 fixed it from 1.2.0 and up it does not work
Object literal may only specify known properties, and 'crewInventory' does not exist in type '{ readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }'.ts(2353)
better-auth.Dxn5Xplu.d.ts(6555, 21): The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }; organizationId?: string | undefin...'
Object literal may only specify known properties, and 'crewInventory' does not exist in type '{ readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }'.ts(2353)
better-auth.Dxn5Xplu.d.ts(6555, 21): The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }; organizationId?: string | undefin...'
@heroui/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @iconify/[email protected]
├── @prisma/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @react-email/[email protected]
├── @reactuses/[email protected]
├── @tailwindcss/[email protected]
├── @tanstack/[email protected]
├── @tanstack/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @vercel/[email protected]
@heroui/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @iconify/[email protected]
├── @prisma/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @react-email/[email protected]
├── @reactuses/[email protected]
├── @tailwindcss/[email protected]
├── @tanstack/[email protected]
├── @tanstack/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @vercel/[email protected]
when does this error happens? on
auth.api.hasPermission
or on the client?on the client
so:
authClient.organization.hasPermission
authClient.organization.hasPermission
It might have todo with the multiple role support you added?

Since it seems you now have to define your role idk its alot of changes to look over

will be fixed on the next release. thanks for the collaboration :))
Thanks, np thank you for all the amazing work better-auth really changed the development speed by alot! especially for someone working alone on a huge project.
should be fixed on
1.2.4-beta.3
Correct this issue is fixed, you might also want to take a look into
Nvm the hasPermission still doesn't work however the issue's in the auth.ts config have been fixed
For everyone wondering about the issue from authClient.organization.inviteMember the temp fix is todo something like:
authClient.organization.inviteMember
Type 'string' is not assignable to type '"member" | "admin" | "owner"'.ts(2322)
it does not allow for a custom role at:
type InferRolesFromOption<O extends OrganizationOptions | undefined> = O extends {
roles: any;
} ? keyof O["roles"] : "admin" | "member" | "owner";
type InferRolesFromOption<O extends OrganizationOptions | undefined> = O extends {
roles: any;
} ? keyof O["roles"] : "admin" | "member" | "owner";
await authClient.organization.hasPermission({
permission: { crewInvite: ["view"] } as any,
});
await authClient.organization.hasPermission({
permission: { crewInvite: ["view"] } as any,
});