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
| Action | Owner | Admin | Member |
|---|---|---|---|
VIEW_WORKSPACE | Yes | Yes | Yes |
UPDATE_WORKSPACE | Yes | Yes | No |
DELETE_WORKSPACE | Yes | No | No |
CREATE_TEAM | Yes | Yes | No |
MANAGE_INVITATIONS | Yes | Yes | No |
VIEW_DOCUMENTS | Yes | Yes | Yes |
CREATE_DOCUMENT | Yes | Yes | Yes |
VIEW_TEAMS | Yes | Yes | Yes |
Team Actions
| Action | WS Owner | Team Owner | Team Admin | Team Member |
|---|---|---|---|---|
VIEW_TEAM | Yes | Yes | Yes | Yes |
UPDATE_TEAM | Yes | Yes | Yes | No |
MANAGE_TEAM_MEMBERS | Yes | Yes | Yes | No |
VIEW_TEAM_MEMBERS | Yes | Yes | Yes | Yes |
DELETE_TEAM | Yes | Yes | No | No |
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:
- Frontend UI — Hide buttons/forms the user can't use
- API Routes — Validate token and forward to backend
- Django Backend — DRF permission classes enforce access (source of truth)
The frontend permission system is for UX — the backend always has the final say.