HyperSaaS
FrontendAuthentication

Permissions

Role-based access control for workspace and team actions.

HyperSaaS implements a permission system that controls what UI elements are visible and what actions are available based on the user's role in a workspace or team.

Permission Context

The UserPermissionContext is computed from the user's relationship to a workspace and optional team:

// lib/roles-permissions.ts
interface UserPermissionContext {
  isWorkspaceOwner: boolean;
  workspaceRole: "admin" | "member" | null;
  isTeamOwner: boolean;
  teamRole: "admin" | "member" | null;
}

Computing Context

export function getUserPermissionContext(
  user: UserDetails,
  workspace: Workspace,
  team?: Team
): UserPermissionContext {
  const membership = workspace.memberships?.find(m => m.user === user.id);

  return {
    isWorkspaceOwner: workspace.owner === user.id,
    workspaceRole: membership?.role ?? null,
    isTeamOwner: team?.created_by === user.id,
    teamRole: team?.memberships?.find(m => m.user === user.id)?.role ?? null,
  };
}

Permission Checks

Actions are checked against the permission context:

export function canUserPerformWorkspaceAction(
  context: UserPermissionContext,
  action: string
): boolean {
  switch (action) {
    case "VIEW_WORKSPACE":
      return context.isWorkspaceOwner || context.workspaceRole !== null;

    case "UPDATE_WORKSPACE":
      return context.isWorkspaceOwner || context.workspaceRole === "admin";

    case "DELETE_WORKSPACE":
      return context.isWorkspaceOwner;

    case "CREATE_TEAM":
      return context.isWorkspaceOwner || context.workspaceRole === "admin";

    case "MANAGE_INVITATIONS":
      return context.isWorkspaceOwner || context.workspaceRole === "admin";

    // ...
  }
}

Available Permissions

Workspace Actions

ActionOwnerAdminMember
VIEW_WORKSPACEYesYesYes
UPDATE_WORKSPACEYesYesNo
DELETE_WORKSPACEYesNoNo
CREATE_TEAMYesYesNo
MANAGE_INVITATIONSYesYesNo
VIEW_DOCUMENTSYesYesYes
CREATE_DOCUMENTYesYesYes
VIEW_TEAMSYesYesYes

Team Actions

ActionWS OwnerTeam OwnerTeam AdminTeam Member
VIEW_TEAMYesYesYesYes
UPDATE_TEAMYesYesYesNo
MANAGE_TEAM_MEMBERSYesYesYesNo
VIEW_TEAM_MEMBERSYesYesYesYes
DELETE_TEAMYesYesNoNo

Usage in Server Components

export default async function WorkspaceDetailsPage({ params }) {
  const { workspaceId } = await params;
  const user = await getCurrentUserServer();
  const workspace = await fetchWorkspace(workspaceId);

  const permissionContext = getUserPermissionContext(user, workspace);
  const canEdit = canUserPerformWorkspaceAction(permissionContext, "UPDATE_WORKSPACE");
  const canManageTeams = canUserPerformWorkspaceAction(permissionContext, "CREATE_TEAM");

  return (
    <div>
      {canEdit && <WorkspaceUpdateForm workspace={workspace} />}
      {canManageTeams && <TeamCreateForm workspaceId={workspaceId} />}
      <WorkspaceMembersList workspace={workspace} canManage={canEdit} />
    </div>
  );
}

Usage in Client Components

Permission flags are passed as props from server components:

// Server component
<KnowledgeBaseList
  knowledgeBases={kbs}
  permissionContext={permissionContext}
/>

// Client component
function KnowledgeBaseList({ knowledgeBases, permissionContext }) {
  const canCreate = canUserPerformWorkspaceAction(permissionContext, "CREATE_DOCUMENT");
  const canDelete = canUserPerformWorkspaceAction(permissionContext, "UPDATE_WORKSPACE");

  return (
    <>
      {canCreate && <CreateKBButton />}
      {knowledgeBases.map(kb => (
        <KBRow key={kb.id} kb={kb} canDelete={canDelete} />
      ))}
    </>
  );
}

Defense in Depth

Permissions are enforced at three levels:

  1. Frontend UI — Hide buttons/forms the user can't use
  2. API Routes — Validate token and forward to backend
  3. Django Backend — DRF permission classes enforce access (source of truth)

The frontend permission system is for UX — the backend always has the final say.

On this page