From casl
Add or review CASL permission checks in Lety 2.0 Frontend — conditional rendering with usePermissionsStore, route protection via AuthRedirect, and sidebar visibility. Triggered when the user needs to guard a feature, component, or route by permission.
How this skill is triggered — by the user, by Claude, or both
Slash command
/casl:caslThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are adding or reviewing **permission-based access control** in the Lety 2.0 Frontend (CASL + Zustand + Next.js 15 App Router).
You are adding or reviewing permission-based access control in the Lety 2.0 Frontend (CASL + Zustand + Next.js 15 App Router).
Priority rule: Always follow CASL docs and the project's established permission patterns. Permissions are loaded from the backend at login and stored in
usePermissionsStore. Never hardcode role names to guard access — always check CASL abilities.
features/permissions/logic/
permissions-store.ts # Zustand store: PureAbility instance + raw rules
features/auth/components/
auth-redirect.tsx # Client component: guards routes and enforces onboarding
src/shared/enums/
permissions.enum.ts # Actions enum (READ, CREATE, UPDATE, DELETE, MANAGE)
resources.enum.ts # TenantResourceObjectEnum, PlatformResourceObjectEnum
Permission flow:
permissions[] (array of { action, subject } rules)usePermissionsStore.setPermissions(rules) builds a PureAbility instancepermissions.can(action, subject) or haveSomePermission(rules)AuthRedirect uses sidebar config permissions to block entire routesAsk the user if not provided:
AGENTS, BILLING, SETTINGS)READ, CREATE, UPDATE, DELETE, MANAGE)AuthRedirect + sidebar configpermissions.can()haveSomePermission()'use client';
import { usePermissionsStore } from '@/features/permissions/logic/permissions-store';
import { Actions } from '@/shared/enums/permissions.enum';
import { TenantResourceObjectEnum } from '@/shared/enums/resources.enum';
export function AgentActions({ agentId }: { agentId: string }) {
const permissions = usePermissionsStore((state) => state.permissions);
const canCreate = permissions.can(Actions.CREATE, TenantResourceObjectEnum.AGENTS);
const canDelete = permissions.can(Actions.DELETE, TenantResourceObjectEnum.AGENTS);
return (
<div>
{canCreate && <button>Create Agent</button>}
{canDelete && <button>Delete</button>}
</div>
);
}
haveSomePermission)'use client';
import { usePermissionsStore } from '@/features/permissions/logic/permissions-store';
import { Actions } from '@/shared/enums/permissions.enum';
import { TenantResourceObjectEnum } from '@/shared/enums/resources.enum';
export function BillingSection() {
const haveSomePermission = usePermissionsStore((state) => state.haveSomePermission);
const canAccessBilling = haveSomePermission([
{ action: Actions.READ, subject: TenantResourceObjectEnum.BILLING },
{ action: Actions.MANAGE, subject: TenantResourceObjectEnum.BILLING },
]);
if (!canAccessBilling) return null;
return <div>{/* billing content */}</div>;
}
import { usePermissionsStore } from '@/features/permissions/logic/permissions-store';
import { Actions } from '@/shared/enums/permissions.enum';
import { TenantResourceObjectEnum } from '@/shared/enums/resources.enum';
// Access store state synchronously outside React
const { permissions } = usePermissionsStore.getState();
if (!permissions.can(Actions.UPDATE, TenantResourceObjectEnum.AGENTS)) {
// handle unauthorized
return;
}
Route protection happens in AuthRedirect via the sidebar config. To protect a new route:
1. Add permissions to the sidebar item config (features/auth/model/consts/sidebar.const.ts):
{
path: '/billing',
label: 'Billing',
icon: CreditCardIcon,
permissions: [
{ action: Actions.READ, subject: TenantResourceObjectEnum.BILLING },
],
},
2. AuthRedirect will automatically block the route if the user lacks the required permissions, redirecting to /403.
If the route is NOT in the sidebar (e.g., a detail page /agents/:id), add an explicit check in the view:
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { usePermissionsStore } from '@/features/permissions/logic/permissions-store';
import { Actions } from '@/shared/enums/permissions.enum';
import { TenantResourceObjectEnum } from '@/shared/enums/resources.enum';
export function AgentDetailView({ agentId }: { agentId: string }) {
const router = useRouter();
const permissions = usePermissionsStore((state) => state.permissions);
useEffect(() => {
if (!permissions.can(Actions.READ, TenantResourceObjectEnum.AGENTS)) {
router.replace('/403');
}
}, [permissions, router]);
// render view...
}
When a user is being impersonated, DELETE actions must be blocked even if the ability allows them. This is already enforced in AbilityFactory on the backend, but verify on the frontend:
const isImpersonating = useAuthStore((state) => !!state.user?.impersonatedUserId);
const permissions = usePermissionsStore((state) => state.permissions);
const canDelete = !isImpersonating && permissions.can(Actions.DELETE, TenantResourceObjectEnum.AGENTS);
When reviewing existing code for permission issues:
user.role === 'Admin' is fragile; use permissions.can() insteadisAdmin && <Component /> bypasses CASL; use ability checksTenantResourceObjectEnum; platform resources use PlatformResourceObjectEnumhaveSomePermission for OR logic — don't chain multiple permissions.can() with || manually| Anti-pattern | Fix |
|---|---|
user.role === 'Agency Owner' | permissions.can(Actions.MANAGE, TenantResourceObjectEnum.X) |
| Hiding UI without checking permissions | Always check ability — UI hiding is not access control |
permissions.can('read', 'all') with strings | Use enums: Actions.READ, TenantResourceObjectEnum.X |
Calling setPermissions anywhere other than login | Only set permissions once at login; never patch mid-session |
Missing null guard on permissions before can() | Store always initializes with new PureAbility([]) — safe to call directly |
Route protection only via CSS display: none | Always redirect unauthorized users — never just hide the UI |
permissions.can()Actions enum and TenantResourceObjectEnum / PlatformResourceObjectEnum — never raw stringsuseEffect redirect (for non-sidebar routes)PermissionsGuardCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub lety-ai/lety-skill-hub --plugin casl