LogoBoring Template

Role based access control

A flexible and secure permission management system for workspaces.

Our RBAC system provides flexible and secure permission management for workspaces, allowing fine-grained control over who can access and modify resources.

Initial Setup

When setting up the project for the first time, you need to seed the database to initialize the RBAC system.

To seed the database with default roles and permissions:

Terminal
# Run the database seed script
npm run db:seed
 
# Expected output:
🌱 Starting database seed...
📊 Initializing RBAC...
 Seed completed successfully

This will create:

  • All default permissions in the database
  • Three default roles (Owner, Admin, Member)
  • Role-permission relationships

Permissions System

Our platform uses a granular permission system to control access to resources. Permissions follow a resource:action format.

Available Permissions

Predefined permissions constants:

lib/permissions.ts
export const PERMISSIONS = {
  VIEW_ITEMS: "view:items",
  CREATE_ITEMS: "create:items",
  UPDATE_ITEMS: "update:items",
  DELETE_ITEMS: "delete:items",
  VIEW_MEMBERS: "view:members",
  CREATE_MEMBERS: "create:members",
  UPDATE_MEMBERS: "update:members",
  DELETE_MEMBERS: "delete:members",
  MANAGE_MEMBERS: "manage:members",
  INVITE_MEMBERS: "invite:members",
  REMOVE_MEMBERS: "remove:members",
  MANAGE_ROLES: "manage:roles",
  MANAGE_WORKSPACE: "manage:workspace",
  DELETE_WORKSPACE: "delete:workspace",
  TRANSFER_OWNERSHIP: "transfer:ownership",
} as const;

Member Permissions

  • view:members - View workspace members

  • create:members - Add new members

  • update:members - Update member details

  • delete:members - Delete members

  • manage:members - Full member management

  • invite:members - Send invitations

  • remove:members - Remove members

Workspace Permissions

  • manage:roles - Manage role assignments

  • manage:workspace - Manage workspace settings

  • delete:workspace - Delete workspace

  • transfer:ownership - Transfer ownership

Item Permissions

  • view:items - View workspace items

  • create:items - Create new items

  • update:items - Update existing items

  • delete:items - Delete items

Default Roles

The system comes with predefined roles that have specific permissions. These roles provide a starting point for access control in your workspaces.

Default Roles Configuration

Predefined roles with their permissions:

lib/roles.ts
const defaultRoles: DefaultRolesType[] = [
  {
    name: "owner",
    description: "Workspace owner",
    permissions: ["*"], // All permissions
  },
  {
    name: "admin",
    description: "Workspace administrator",
    permissions: [
      "view:members",
      "create:members",
      "update:members",
      "delete:members",
    ],
  },
  {
    name: "member",
    description: "Regular member",
    permissions: ["view:members"],
  },
];

Owner

All Permissions

Workspace owner with full administrative access

Permissions: All permissions (*)

Admin

Limited Permissions

Workspace administrator with member management capabilities

Permissions: view:members, create:members, update:members, delete:members

Member

Basic Access

Regular workspace member with basic access

Permissions: view:members

The RBAC system is automatically initialized during database seeding. Permissions and roles are created with predefined settings but can be modified through the admin interface.

Database Schema

The RBAC system uses a relational database structure to manage roles and permissions:

server/db/schema/rbac.ts
export const permissions = pgTable("permissions", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  name: text("name").notNull().unique(),
  description: text("description"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at"),
});
 
export const roles = pgTable("roles", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  name: text("name").notNull().unique(),
  description: text("description"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at"),
});
 
export const rolePermissions = pgTable(
  "role_permissions",
  {
    roleId: text("role_id")
      .notNull()
      .references(() => roles.id, { onDelete: "cascade" }),
    permissionId: text("permission_id")
      .notNull()
      .references(() => permissions.id, { onDelete: "cascade" }),
  },
  (t) => ({
    pk: primaryKey({ columns: [t.roleId, t.permissionId] }),
  })
);

Technical Implementation

The RBAC system uses a many-to-many relationship between roles and permissions, implemented using PostgreSQL through Drizzle ORM. This allows for flexible permission management and easy role assignments.

Permission Checking

Check if a user has a specific permission:

lib/permissions.ts
// Check if user has a specific permission
const hasAccess = await hasPermission(userId, workspaceId, "manage:members");
 
// Check multiple permissions
const canManageWorkspace = await hasPermissions(userId, workspaceId, [
  "manage:workspace",
  "manage:roles",
]);

Implementation Details

The permission checking functions are implemented as follows:

lib/permissions.ts
export async function hasPermission(
  userId: string,
  workspaceId: string,
  permission: string
): Promise<boolean> {
  // Get user's role in the workspace
  const [member] = await db
    .select({
      roleId: workspaceMembers.roleId,
    })
    .from(workspaceMembers)
    .where(
      and(
        eq(workspaceMembers.userId, userId),
        eq(workspaceMembers.workspaceId, workspaceId)
      )
    )
    .limit(1);
 
  if (!member) return false;
 
  // Check if the role has the wildcard permission
  const hasWildcard = await db
    .select()
    .from(rolePermissions)
    .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id))
    .where(
      and(eq(rolePermissions.roleId, member.roleId), eq(permissions.name, "*"))
    )
    .limit(1);
 
  if (hasWildcard.length > 0) return true;
 
  // Check for specific permission
  const hasSpecificPermission = await db
    .select()
    .from(rolePermissions)
    .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id))
    .where(
      and(
        eq(rolePermissions.roleId, member.roleId),
        eq(permissions.name, permission)
      )
    )
    .limit(1);
 
  return hasSpecificPermission.length > 0;
}

React Components

Authorization Component

Create a component to conditionally render UI based on permissions:

components/authorized.tsx
import { hasPermission } from "@/lib/permissions";
 
interface AuthorizedProps {
  permission: string;
  workspaceId: string;
  fallback?: React.ReactNode;
  children: React.ReactNode;
}
 
export async function Authorized({
  permission,
  workspaceId,
  fallback = null,
  children,
}: AuthorizedProps) {
  const { user } = await getCurrentUser();
 
  if (!user) {
    return fallback;
  }
 
  const hasAccess = await hasPermission(user.id, workspaceId, permission);
 
  if (!hasAccess) {
    return fallback;
  }
 
  return <>{children}</>;
}

Usage example:

Example: Permission-based UI rendering
<Authorized
  permission="delete:workspace"
  workspaceId={workspace.id}
  fallback={<p>You don't have permission to delete this workspace</p>}
>
  <Button variant="destructive" onClick={handleDelete}>
    Delete Workspace
  </Button>
</Authorized>

Client-Side Permission Hook

For client components that need to check permissions:

hooks/use-permissions.tsx
import { useState, useEffect } from "react";
import { trpc } from "@/trpc/client";
 
export function usePermission(workspaceId: string, permission: string) {
  const { data, isLoading } = trpc.permissions.check.useQuery({
    workspaceId,
    permission,
  });
 
  return {
    hasPermission: data?.hasPermission ?? false,
    isLoading,
  };
}

Usage example:

Example: Client-side permission check
"use client";
 
import { usePermission } from "@/hooks/use-permissions";
 
export function DeleteWorkspaceButton({
  workspaceId,
}: {
  workspaceId: string;
}) {
  const { hasPermission, isLoading } = usePermission(
    workspaceId,
    "delete:workspace"
  );
 
  if (isLoading) {
    return <Button disabled>Loading...</Button>;
  }
 
  if (!hasPermission) {
    return null;
  }
 
  return (
    <Button variant="destructive" onClick={handleDelete}>
      Delete Workspace
    </Button>
  );
}

Best Practices

Security

  • Always check permissions on both client and server
  • Use middleware for API routes to validate permissions
  • Avoid hardcoding permission checks in UI components
  • Implement least privilege principle for roles

Performance

  • Cache permission results when appropriate
  • Use batch permission checking for multiple permissions
  • Optimize database queries for permission checks
  • Consider denormalization for frequently checked permissions

Maintenance

  • Use constants for permission names
  • Document new permissions when adding them
  • Create migration scripts for permission changes
  • Test permission changes thoroughly

Additional Resources

Member Management

See the Members Management documentation for more information on member roles.

Multi-Workspace

Learn about Multi-Workspace management in the application.

On this page