Makerkit implements granular RBAC for team members. This allows you to define roles and permissions for each team member - giving you fine-grained control over who can access what.
Makerkit implements two tables for roles and permissions:
roles
table: This table stores the roles for each team member.role_permissions
table: This table stores the permissions for each role.
The table role_permissions
has the following schema:
id
: The unique identifier for the role permission.role
: The role for the team member.permission
: The permission for the role.
The roles
table has the following schema:
name
: The name of the role. This must be unique.hierarchy_level
: The hierarchy level of the role.
We can use hierarchy_level
to define the hierarchy of roles. For example, an admin
role can have a higher hierarchy level than a member
role. This will help you understand if a role has more permissions than another role.
And an enum for permissions app_permissions
:
app_permissions
enum: This enum stores the permissions for each role.
By default, Makerkit comes with two roles: owner
and member
- and the following permissions:
create type public.app_permissions as enum(
'roles.manage',
'billing.manage',
'settings.manage',
'members.manage',
'invites.manage'
);
You can add more roles and permissions as needed.
Default roles and permissions
The default roles are defined as follows:
- Members with
owner
role have full access to the application. - Members with
member
role have the following permissions:members.manage
andinvites.manage
.
Adding new roles and permissions
To add new permissions, you can update the app_permissions
enum:
-- insert new permissions
alter type public.app_permissions add value 'tasks.write';
alter type public.app_permissions add value 'tasks.delete';
commit;
In the above, we added two new permissions: tasks.write
and tasks.delete
.
You can assign these permissions to roles in the role_permissions
table for fine-grained access control:
insert into public.role_permissions (role, permission) values ('owner', 'tasks.write');
insert into public.role_permissions (role, permission) values ('owner', 'tasks.delete');
Of course - you will need to enforce these permissions in your application code and RLS.
Using roles and permissions in RLS
To check if a user has a certain permission on an account, we can use the function has_permission
- which you can use in your RLS to enforce permissions.
In the below, we create an RLS policy insert_tasks
on the tasks
table to check if a user can insert a new task. We use public.has_permission
to check if the current user has the permission tasks.write
:
create policy insert_tasks on public.tasks
for insert
with check (
public.has_permission(auth.uid(), account_id, 'tasks.write'::app_permissions)
);
And now we can also add a policy to check if a user can delete a task:
create policy delete_tasks on public.tasks
for delete
using (
public.has_permission(auth.uid(), account_id, 'tasks.delete'::app_permissions)
);
Using roles and permissions in application code
You can use the exact same function has_permission
in your application code to check if a user has a certain permission. You will call the function with the Supabase RPC method:
async function hasPermissionToInsertTask(userId: string, accountId: string) {
const { data: hasPermission, error } = await client.rpc('has_permission', {
user_id: userId,
account_id: accountId,
permission: 'tasks.write',
});
if (error || !hasPermission) {
throw new Error(`User has no permission to insert task`);
}
}
You can now use hasPermissionToInsertTask
to check if a user has permission to insert a task anywhere in your application code - provided you obtain the user and account IDs.
You can use this function to gate access to certain pages, or verify the user permissions before performing some server-side requests.
Of course, it's always worth making sure RLS is enforced on the database level as well.
Using permissions client-side
While checks must be done always server-side, it is useful to have the permissions available client-side for UI purposes. For example, you may want to hide a certain button if the user does not have the permission to perform an action.
We fetch the permissions as part of the Account Workspace API - which is available to the layout around the account routes.
This API fetches the permissions for the current user and account and makes them available to the client-side simply by passing it from a page to the client components that require it.
Let's assume you have a page, and you want to check if the user has the permission to write tasks:
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
export default function TasksPage() {
const data = await loadTeamWorkspace();
const permissions = data.account.permissions; // string[]
const canWriteTasks = permissions.includes('tasks.write');
return (
<div>
{canWriteTasks && <button>Create Task</button>}
// other UI elements // ...
</div>
);
}
You can also pass the permissions list to the components that need it as a prop.
This way, you can gate access to certain UI elements based on the user's permissions.
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
export default function TasksPage() {
const data = await loadTeamWorkspace();
const permissions = data.account.permissions; // string[]
return (
<div>
<TaskList permissions={permissions} />
</div>
);
}
Similarly, you can use the permissions to gate access to certain routes or pages.
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
export default function TasksPage() {
const data = await loadTeamWorkspace();
const permissions = data.account.permissions; // string[]
if (!permissions.includes('tasks.read')) {
return <ErrorPage message="You do not have permission to write tasks" />;
}
return (
<div>
<TaskList permissions={permissions} />
</div>
);
}
Easy as that!