Skip to main content
CharleOS uses a flexible permission system based on access levels and work types. All permission logic is centralized in lib/permissions.ts.

Permission Model

Two-Tier System

CharleOS combines two concepts for access control:

Access Level

System-wide permission tier
  • Admin: Full access (Luke)
  • Manager: Management features (Simon, Andre, Ben, Nic)
  • Staff: Standard access (everyone else)

Work Type

Role-based feature access
  • development/design/qa: IC features
  • pm: Project management features
  • csm: Client success features
  • sdr: Limited features

Combined Logic

The permission system uses both factors:
// Example: Can view manager dashboard?
function canViewManagerDashboard(user) {
  return user.accessLevel === "admin" || user.accessLevel === "manager";
}

// Example: Can view IC dashboard?
function canViewICDashboard(user) {
  const isIC = ["development", "design", "qa"].includes(user.workType);
  return isIC && user.accessLevel === "staff";
}
Result: A developer with manager access level sees the Manager Dashboard, not the IC Dashboard.

Permission Functions

All permission checks are in lib/permissions.ts:

Access Level Checks

import { isAdmin, isAdminOrManager, hasManagerAccess } from "@/lib/permissions";

// Check if user is admin
if (isAdmin(user)) {
  // Show admin-only features
}

// Check if user is admin or manager
if (isAdminOrManager(user)) {
  // Show management features
}

// Alias for isAdminOrManager
if (hasManagerAccess(user)) {
  // Show management features
}

Work Type Checks

import { isIC, isPM, isCSM } from "@/lib/permissions";

// Check if user is an Individual Contributor (dev/design/qa)
if (isIC(user)) {
  // Show IC-specific features
}

// Check if user is a PM
if (isPM(user)) {
  // Show PM features
}

// Check if user is a CSM
if (isCSM(user)) {
  // Show CSM features
}

Dashboard Access

Dashboard Types

CharleOS has 5 dashboard types based on role:
DashboardWho Sees ItPermission Check
Manager DashboardAdmin/Manager access levelcanViewManagerDashboard()
PM DashboardPMs (staff level)canViewPMDashboard()
CSM DashboardCSMs (staff level)canViewCSMDashboard()
IC DashboardDevelopers/Designers/QA (staff level)canViewICDashboard()
Executive DashboardUsers with hasExecutiveAccess flagcanViewCommercialsDashboard()
Code:
import {
  canViewManagerDashboard,
  canViewPMDashboard,
  canViewCSMDashboard,
  canViewICDashboard,
} from "@/lib/permissions";

function getDashboardForUser(user) {
  if (canViewManagerDashboard(user)) return "/dashboard/manager";
  if (canViewPMDashboard(user)) return "/dashboard/pm";
  if (canViewCSMDashboard(user)) return "/dashboard/csm";
  if (canViewICDashboard(user)) return "/dashboard/ic";
  return "/dashboard";  // Fallback
}
Sidebar items are shown/hidden based on permissions:
import {
  canViewClients,
  canViewTasks,
  canViewSchedule,
  canViewMyTasks,
  canViewMySchedule,
  canViewCapacity,
  canViewReports,
  canViewAdmin,
} from "@/lib/permissions";

// Example: Sidebar navigation
const sidebarItems = [
  {
    label: "My Tasks",
    href: "/my-tasks",
    visible: canViewMyTasks(user),  // ICs only
  },
  {
    label: "Clients",
    href: "/clients",
    visible: canViewClients(user),  // Admin/Manager/PM/CSM
  },
  {
    label: "Schedule",
    href: "/schedule",
    visible: canViewSchedule(user),  // Admin/Manager/PM
  },
  {
    label: "Reports",
    href: "/reports",
    visible: canViewReports(user),  // Admin/Manager only
  },
  {
    label: "Admin",
    href: "/admin",
    visible: canViewAdmin(user),  // Admin only
  },
];

Operational Permissions

Task Management

import {
  canCreateTasks,
  canEditTasks,
  canDeleteTasks,
  canAssignSubtasks,
} from "@/lib/permissions";

// Can create tasks? (Admin/Manager/CSM/PM)
if (canCreateTasks(user)) {
  return <CreateTaskButton />;
}

// Can edit tasks? (Admin/Manager/PM/CSM - edits task-level fields)
// Allows editing: title, description, estimatedTime, clientId, pmId
// Does NOT allow: creating/deleting tasks, assigning subtasks
// Note: ICs (developers, designers, QA) cannot edit task-level fields
if (canEditTasks(user)) {
  return <EditTaskButton />;
}

// Can delete tasks? (Admin/Manager only)
if (canDeleteTasks(user)) {
  return <DeleteTaskButton />;
}

// Can assign subtasks? (Admin/Manager/PM)
if (canAssignSubtasks(user)) {
  return <AssignSubtaskButton />;
}

Quote Management

import {
  canCreateQuotes,
  canDeleteQuotes,
  canEditQuote,
  canApproveQuote,
  canManageQuotes,
} from "@/lib/permissions";

// Can create quotes? (Admin/Manager/CSM)
if (canCreateQuotes(user)) {
  return <CreateQuoteButton />;
}

// Can delete quotes? (Admin/Manager/CSM)
if (canDeleteQuotes(user)) {
  return <DeleteQuoteButton />;
}

// Can edit quote details? (Admin/Manager only)
if (canEditQuote(user)) {
  return <EditQuoteDetailsForm />;
}

// Can approve quotes? (Admin/Manager/CSM)
if (canApproveQuote(user)) {
  return <ApproveQuoteButton />;
}

Client Management

import {
  canEditClients,
  canEditClientSchedule,
  canManageClientUsers,
} from "@/lib/permissions";

// Can edit client details? (Admin/Manager only)
if (canEditClients(user)) {
  return <EditClientForm />;
}

// Can edit client schedule/blocks? (Admin/Manager/PM/CSM)
if (canEditClientSchedule(user)) {
  return <ClientScheduleEditor />;
}

// Can manage client portal users? (Admin/Manager/CSM)
if (canManageClientUsers(user)) {
  return <ClientUserManagement />;
}

Project Management

import {
  canCreateProjects,
  canEditProjectBudget,
  canManageProjectTasks,
  canAddProjectPhases,
} from "@/lib/permissions";

// Can create projects? (Admin/Manager only)
if (canCreateProjects(user)) {
  return <CreateProjectButton />;
}

// Can edit project budget? (Admin/Manager only)
if (canEditProjectBudget(user)) {
  return <EditBudgetForm />;
}

// Can manage project tasks? (Admin/Manager/PM)
if (canManageProjectTasks(user)) {
  return <ProjectTaskManager />;
}

// Can add phases? (Admin/Manager/PM)
if (canAddProjectPhases(user)) {
  return <AddPhaseButton />;
}

Implementation Patterns

Protected Server Component

// app/(dashboard)/admin/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { canViewAdmin } from "@/lib/permissions";

export default async function AdminPage() {
  const { data: session } = await auth.api.getSession({
    headers: headers(),
  });

  if (!session) {
    redirect("/auth");
  }

  // Check permission
  if (!canViewAdmin(session.user)) {
    redirect("/dashboard");  // Redirect to safe page
  }

  return <AdminPanel />;
}

Protected API Route

// app/api/admin/users/route.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { canViewAdmin } from "@/lib/permissions";

export async function GET(req: Request) {
  const { data: session } = await auth.api.getSession({
    headers: headers(),
  });

  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  if (!canViewAdmin(session.user)) {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }

  // Fetch admin data...
}

Conditional UI Rendering

"use client";

import { useSession } from "@/lib/auth-client";
import { canCreateTasks } from "@/lib/permissions";

export function TaskActions() {
  const { data: session } = useSession();

  if (!session) return null;

  return (
    <div>
      {canCreateTasks(session.user) && (
        <CreateTaskButton />
      )}
      
      {canEditTasks(session.user) && (
        <EditTaskButton />
      )}
      
      {canDeleteTasks(session.user) && (
        <DeleteTaskButton />
      )}
    </div>
  );
}

Complete Permission Reference

Dashboard Access

FunctionWho Gets Access
canViewManagerDashboardAdmin, Manager
canViewPMDashboardPM (staff level)
canViewCSMDashboardCSM (staff level)
canViewICDashboardDev/Design/QA (staff level)
canViewCommercialsDashboardUsers with hasExecutiveAccess flag
FunctionWho Gets Access
canViewMyTasksICs (dev/design/qa)
canViewMyScheduleICs (dev/design/qa)
canViewMyTimeTrackingICs (dev/design/qa)
canViewClientsAdmin, Manager, PM, CSM
canViewTasksAdmin, Manager, PM, CSM, ICs
canViewScheduleAdmin, Manager, PM
canViewQuotesAdmin, Manager, PM, CSM, ICs
canViewProjectsAdmin, Manager, PM, ICs
canViewTimeTrackingAdmin, Manager
canViewCapacityAdmin, Manager
canViewAnnualLeaveAdmin, Manager
canViewReportsAdmin, Manager
canViewAdminAdmin only
canViewKnowledgeBaseEveryone
canViewRagEveryone
canViewHelpDeskAdmin, Manager, CSM

Operational Permissions

FunctionWho Gets AccessWhat It Allows
canCreateTasksAdmin, Manager, CSM, PMCreate new tasks
canEditTasksAdmin, Manager, PM, CSMEdit task title, description, estimatedTime, clientId, pmId
canDeleteTasksAdmin, ManagerDelete tasks
canAssignSubtasksAdmin, Manager, PM
canCreateQuotesAdmin, Manager, CSM
canDeleteQuotesAdmin, Manager, CSM
canEditQuoteAdmin, Manager
canApproveQuoteAdmin, Manager, CSM
canManageQuotesAdmin, Manager, PM, CSM
canEditClientsAdmin, Manager
canEditClientScheduleAdmin, Manager, PM, CSM
canManageClientUsersAdmin, Manager, CSM
canCreateProjectsAdmin, Manager
canEditProjectBudgetAdmin, Manager
canManageProjectTasksAdmin, Manager, PM
canAddProjectPhasesAdmin, Manager, PM
canEditRagAdmin, Manager
canViewActivityLogAdmin, Manager

Adding New Permissions

1

Add Permission Function

Add to lib/permissions.ts:
export function canExportData(user: UserPermissionContext): boolean {
  return isAdmin(user);
}
2

Use in Components

import { canExportData } from "@/lib/permissions";

if (canExportData(user)) {
  return <ExportButton />;
}
3

Protect Routes

if (!canExportData(session.user)) {
  return Response.json({ error: "Forbidden" }, { status: 403 });
}

Best Practices

Client-side checks are for UI only. Always verify on the server:
// ✅ Good: Check on server
export async function GET(req: Request) {
  const { data: session } = await auth.api.getSession({ headers: headers() });
  if (!canViewAdmin(session.user)) {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }
  // ... return data
}

// ❌ Bad: Only checking client-side
"use client";
function AdminPanel() {
  if (!canViewAdmin(user)) return null;
  // ... fetch admin data (anyone can call the API!)
}
Don’t check accessLevel or workType directly:
// ✅ Good: Use specific function
if (canCreateTasks(user)) { ... }

// ❌ Bad: Direct check
if (user.accessLevel === "admin" || user.workType === "csm") { ... }
For pages, redirect unauthorized users:
// ✅ Good: Redirect
if (!canViewAdmin(user)) {
  redirect("/dashboard");
}

// ❌ Bad: Return null (shows blank page)
if (!canViewAdmin(user)) {
  return null;
}