LogoBoring Template

Members Management

Manage workspace members, roles, and access control in your application.

This feature allows workspace administrators to manage members, assign roles, and control access to workspace resources. It is protected by authentication, authorization, and rate limiting. Learn more about authentication, authorization, and rate limiting.

Member Schema

Database Definition

The member management system uses a PostgreSQL table with the following structure:

server/db/schema/members.ts
export const workspaceMembers = pgTable(
  "workspaceMembers",
  {
    // User reference
    userId: text("user_id")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
 
    // Workspace reference
    workspaceId: text("workspace_id")
      .notNull()
      .references(() => workspaces.id, { onDelete: "cascade" }),
 
    // Role assignment
    roleId: text("role_id")
      .notNull()
      .references(() => roles.id),
 
    // Timestamps
    createdAt: timestamp("created_at").notNull().defaultNow(),
    updatedAt: timestamp("updated_at"),
  },
  (t) => ({
    pk: primaryKey({ columns: [t.userId, t.workspaceId] }),
  })
);

Access Control

Access control ensures that only authorized users can access workspace resources. The MemberProvider component handles workspace access verification.

Member Provider

Authorization wrapper for workspace routes:

components/member-provider.tsx
export async function MemberProvider({ children, slug }: MemberProviderProps) {
  // 1. Check authentication
  const { user } = await getCurrentUser();
  if (!user) {
    return redirect(createRoute("sign-in").href);
  }
 
  // 2. Verify workspace exists
  const workspace = await db.query.workspaces.findFirst({
    where: eq(workspaces.slug, slug),
    with: {
      members: {
        columns: {
          userId: true,
        },
      },
    },
  });
 
  if (!workspace) {
    return notFound();
  }
 
  // 3. Check membership
  if (!workspace.members.some((member) => member.userId === user.id)) {
    return (
      <RestrictedContent
        title="Unauthorized"
        description="You are not authorized to access this workspace."
      />
    );
  }
 
  return <>{children}</>;
}

Implementation

Fetching Members

Get all members of a workspace:

Example: Fetching workspace members
import { trpc } from "@/trpc/client";
 
// Inside your component
const { data: members, isLoading } = trpc.members.getMany.useQuery({
  slug: workspace.slug,
});
 
// Render members list
return (
  <div>
    {members?.map((member) => (
      <div key={member.id} className="flex items-center justify-between py-4">
        <div className="flex items-center gap-3">
          <Avatar>
            <AvatarImage src={member.user.image || undefined} />
            <AvatarFallback>{getInitials(member.user.name)}</AvatarFallback>
          </Avatar>
          <div>
            <p className="font-medium">{member.user.name}</p>
            <p className="text-sm text-muted-foreground">{member.user.email}</p>
          </div>
        </div>
        <Badge>{member.role.name}</Badge>
      </div>
    ))}
  </div>
);

Update Member

Update member roles and permissions:

Example: Updating member role
import { useUpdateMember } from "@/hooks/members";
import { toast } from "sonner";
 
// Inside your component
const { server_updateMember, isPending } = useUpdateMember({
  onSuccess: () => {
    toast.success("Member role updated successfully");
  },
  onError: (error) => {
    toast.error(error.message);
  },
});
 
// Update member role
await server_updateMember({
  role: "admin",
  userId: member.id,
  slug: workspace.slug,
});

Delete Member

Remove member from workspace:

Example: Removing a member
import { useDeleteMember } from "@/hooks/members";
import { toast } from "sonner";
 
// Inside your component
const { server_deleteMember, isPending } = useDeleteMember({
  onSuccess: () => {
    toast.success("Member removed successfully");
  },
  onError: (error) => {
    toast.error(error.message);
  },
});
 
// Remove member
await server_deleteMember({
  userId: member.id,
  slug: workspace.slug,
});

Leave Workspace

Member leaves workspace voluntarily:

Example: Leaving a workspace
import { useLeaveWorkspace } from "@/hooks/members";
import { toast } from "sonner";
 
// Inside your component
const { server_leaveWorkspace, isPending } = useLeaveWorkspace({
  onSuccess: () => {
    toast.success("You have left the workspace");
    // Redirect to workspaces list
    router.push(createRoute("workspaces").href);
  },
  onError: (error) => {
    toast.error(error.message);
  },
});
 
// Leave workspace
await server_leaveWorkspace({
  slug: workspace.slug,
});

Member Roles

Roles define what actions members can perform within a workspace. Each member is assigned a role that determines their permissions.

Default Roles

The system includes several default roles:

Owner

  • Full control of workspace
  • Can delete workspace
  • Can transfer ownership
  • Can manage all members

Admin

  • Can manage workspace settings
  • Can manage members (except owner)
  • Can invite new members
  • Can create and manage content

Member

  • Can view workspace
  • Can create and edit own content
  • Limited access to settings
  • No member management

Guest

  • View-only access to workspace
  • Cannot create or edit content
  • No access to settings
  • Temporary access

Role Schema

server/db/schema/roles.ts
export const roles = pgTable("roles", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  name: text("name").notNull(),
  description: text("description"),
  permissions: text("permissions").notNull().$type<string[]>(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at"),
});

Security Features

Access Control

  • Role-based permissions
  • Workspace membership verification
  • Automatic cleanup on deletion
  • Secure role updates

Protection

  • Rate limiting on actions
  • Input validation
  • Permission checks
  • Audit logging

Member Management UI

Members Table

Create a table to display and manage workspace members:

components/members-table.tsx
export function MembersTable({ members, currentUser }: MembersTableProps) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Member</TableHead>
          <TableHead>Role</TableHead>
          <TableHead>Joined</TableHead>
          <TableHead>Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {members.map((member) => (
          <TableRow key={member.userId}>
            <TableCell>
              <div className="flex items-center gap-3">
                <Avatar>
                  <AvatarImage src={member.user.image || undefined} />
                  <AvatarFallback>
                    {getInitials(member.user.name)}
                  </AvatarFallback>
                </Avatar>
                <div>
                  <p className="font-medium">{member.user.name}</p>
                  <p className="text-sm text-muted-foreground">
                    {member.user.email}
                  </p>
                </div>
              </div>
            </TableCell>
            <TableCell>
              <RoleBadge role={member.role} />
            </TableCell>
            <TableCell>
              <time dateTime={member.createdAt.toISOString()}>
                {formatDate(member.createdAt)}
              </time>
            </TableCell>
            <TableCell>
              {currentUser.id !== member.userId && (
                <MemberActions
                  member={member}
                  currentUserRole={currentUser.role}
                />
              )}
              {currentUser.id === member.userId && <LeaveWorkspaceButton />}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

Role Badge Component

Display the member's role with appropriate styling:

components/role-badge.tsx
export function RoleBadge({ role }: { role: string }) {
  const variant =
    role === "owner"
      ? "default"
      : role === "admin"
      ? "outline"
      : role === "member"
      ? "secondary"
      : "ghost";
 
  return <Badge variant={variant}>{role}</Badge>;
}

Best Practices

Security

  • Always check permissions before allowing member operations
  • Implement audit logging for sensitive actions
  • Use transactions for operations that modify multiple tables
  • Verify workspace ownership before transferring

User Experience

  • Provide clear feedback for role changes
  • Confirm destructive actions (removal)
  • Show user-friendly error messages
  • Display role permissions clearly

Performance

  • Optimize queries for large member lists
  • Implement pagination for workspaces with many members
  • Cache role permissions when appropriate
  • Use optimistic updates for better UX

Additional Resources

Multi-Workspace Management

See the Multi-Workspace documentation for more information on workspace management.

Invitations

Learn about the Invitation System for adding new members to workspaces.

Authorization

Understand Authorization concepts for securing your application.

On this page