// Use this skill when architecting new features within the RBAC modular permissions system. Guides architects through workspace design, feature definition, permission mapping, and role hierarchy. Critical for understanding Owner/Super Admin special roles, feature independence, and multi-tenant context isolation. Invoke when creating PRDs that involve permissions, workspaces, features, or role-based access control.
| name | rbac-permissions-architect |
| description | Use this skill when architecting new features within the RBAC modular permissions system. Guides architects through workspace design, feature definition, permission mapping, and role hierarchy. Critical for understanding Owner/Super Admin special roles, feature independence, and multi-tenant context isolation. Invoke when creating PRDs that involve permissions, workspaces, features, or role-based access control. |
This skill enables the Architect Agent to design features that correctly integrate with the project's RBAC modular permissions system defined in concepto-base.md. It provides the necessary understanding of workspaces, features, permissions, and special roles to create compliant PRDs and entity designs.
Use this skill when:
Before architecting any feature, understand these immutable principles:
WORKSPACES (Containers)
โ
FEATURES (Modules)
โ
RBAC (Access Control)
permissions-management is always activeQuestions to answer:
Reference files to consult:
Steps:
boards, cards, comments){resource}.{action} (e.g., boards.create, cards.move).read)Example permission matrix:
Feature: kanban
Resources:
โโ boards
โโ cards
โโ card_comments
Permissions:
โโ boards.create
โโ boards.read โ Grants visibility of Kanban in UI
โโ boards.update
โโ boards.delete
โโ cards.create
โโ cards.read
โโ cards.update
โโ cards.delete
โโ cards.move โ Custom action
โโ card_comments.create
โโ card_comments.delete
Reference file to consult:
โก NEW: Authorization Layer Integration
After defining the permission matrix, specify how CASL (the project's authorization library) will implement these permissions.
Questions to answer:
CASL Subjects (PascalCase, Singular):
Map each database resource to a CASL subject:
// Database resources โ CASL subjects
'boards' โ 'Board'
'cards' โ 'Card'
'card_comments' โ 'Comment'
Rules:
CASL Actions:
Standard actions align with permissions:
'create' โ boards.create
'read' โ boards.read
'update' โ boards.update
'delete' โ boards.delete
Custom actions:
'move' โ cards.move
'assign' โ cards.assign
Ability Builder Template:
Include this in your PRD's entities.ts:
// ===== CASL Integration =====
import { AbilityBuilder, createMongoAbility, MongoAbility } from '@casl/ability';
// Define CASL types
type Subjects = 'Board' | 'Card' | 'Comment' | 'all';
type Actions = 'create' | 'read' | 'update' | 'delete' | 'move' | 'manage';
export type AppAbility = MongoAbility<[Actions, Subjects]>;
/**
* Build CASL ability from user permissions
*
* @param user - Current user
* @param workspace - Current workspace context
* @param permissions - User's permissions in this workspace
*/
export function defineAbilitiesFor(
user: User,
workspace: Workspace,
permissions: Permission[]
): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
// ===== Owner: Full access bypass =====
if (user.id === workspace.owner_id) {
can('manage', 'all');
return build();
}
// ===== Super Admin: Full access with restrictions =====
const orgId = workspace.parent_id || workspace.id;
const isSuperAdmin = user.superAdminOrgs?.includes(orgId);
if (isSuperAdmin) {
can('manage', 'all');
// Explicit restrictions (Super Admin CANNOT do these)
cannot('delete', 'Organization');
cannot('modify', 'User', { isOwner: true });
cannot('assign', 'SuperAdminRole');
return build();
}
// ===== Normal users: Map granular permissions =====
permissions.forEach(perm => {
const [resource, action] = perm.full_name.split('.');
const subject = mapResourceToSubject(resource);
can(action as Actions, subject);
});
return build();
}
/**
* Map database resources to CASL subjects
*/
function mapResourceToSubject(resource: string): Subjects {
const mapping: Record<string, Subjects> = {
'boards': 'Board',
'cards': 'Card',
'card_comments': 'Comment',
};
return mapping[resource] || 'all';
}
React Integration Pattern:
Specify how UI components will use CASL:
// ===== React Context (features/rbac/context/AbilityContext.tsx) =====
import { createContext } from 'react';
import { AppAbility } from '../entities';
export const AbilityContext = createContext<AppAbility | null>(null);
// ===== Custom Hook (features/rbac/hooks/useAppAbility.ts) =====
import { useContext } from 'react';
import { AbilityContext } from '../context/AbilityContext';
export function useAppAbility() {
const ability = useContext(AbilityContext);
if (!ability) throw new Error('AbilityContext not provided');
return ability;
}
// ===== Usage in Components =====
import { Can } from '@casl/react';
import { useAppAbility } from '@/features/rbac/hooks/useAppAbility';
function KanbanBoard() {
const ability = useAppAbility();
return (
<div>
{/* Declarative visibility with Can component */}
<Can I="create" a="Board" ability={ability}>
<CreateBoardButton />
</Can>
{/* Programmatic check */}
{ability.can('delete', 'Board') && (
<DeleteBoardButton />
)}
</div>
);
}
โ ๏ธ CRITICAL: CASL vs RLS
CASL and RLS serve different purposes:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CLIENT-SIDE (CASL) โ
โ - UX authorization โ
โ - Show/hide UI elements โ
โ - Enable/disable actions โ
โ - NOT security (can be bypassed) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP Request
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SERVER-SIDE (RLS) โ
โ - REAL security โ
โ - Database-level enforcement โ
โ - Cannot be bypassed โ
โ - Final source of truth โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Both are mandatory. CASL improves UX, RLS enforces security.
Deliverables for Phase 2.5:
In your PRD, include:
defineAbilitiesFor() function in entities.tsmapResourceToSubject() helper<Can>, hooks)Reference:
Questions to answer:
Critical rules:
Reference file to consult:
Questions to answer:
permissions-management)Implementation in entities.ts:
// Feature definition (goes in global features catalog)
export const KanbanFeatureSchema = z.object({
id: z.string().uuid(),
slug: z.literal('kanban'),
name: z.string(),
description: z.string(),
category: z.string(),
is_mandatory: z.boolean().default(false),
});
// Workspace feature activation
export const WorkspaceFeatureSchema = z.object({
workspace_id: z.string().uuid(),
feature_id: z.string().uuid(),
enabled: z.boolean().default(true),
config: z.record(z.unknown()).optional(), // JSONB config
});
Reference file to consult:
Design how users see this feature in the UI:
CASL Integration for Visibility:
Use CASL's <Can> component and ability.can() checks for UI visibility:
// ===== Feature-Level Visibility =====
import { useAppAbility } from '@/features/rbac/hooks/useAppAbility';
function useFeatureVisibility(featureSlug: string) {
const { user, workspace } = useWorkspace();
const ability = useAppAbility();
// Owner/Super Admin bypass
if (user.isOwner || user.isSuperAdmin) {
return isFeatureActive(featureSlug, workspace);
}
// Check if user has ANY permission from feature
const feature = getFeature(featureSlug);
const hasPermission = feature.resources.some(resource => {
const subject = mapResourceToSubject(resource);
return ability.can('read', subject);
});
return hasPermission;
}
// ===== Usage in Navigation =====
function Navigation() {
const showKanban = useFeatureVisibility('kanban');
return (
<nav>
{showKanban && (
<NavItem href="/kanban">Kanban</NavItem>
)}
</nav>
);
}
Component-Level Visibility with <Can>:
import { Can } from '@casl/react';
import { useAppAbility } from '@/features/rbac/hooks/useAppAbility';
function KanbanBoard({ board }: { board: Board }) {
const ability = useAppAbility();
return (
<div>
<h1>{board.name}</h1>
{/* Declarative: Create Button */}
<Can I="create" a="Card" ability={ability}>
<CreateCardButton boardId={board.id} />
</Can>
{/* Declarative: Delete Button */}
<Can I="delete" a="Board" ability={ability}>
<DeleteBoardButton boardId={board.id} />
</Can>
{/* Programmatic: Conditional UI */}
<div className="actions">
{ability.can('update', 'Board') && (
<EditBoardButton board={board} />
)}
{ability.can('move', 'Card') && (
<DragAndDropHandle />
)}
</div>
{/* Inverted: Show message when user CANNOT do something */}
<Can not I="create" a="Card" ability={ability}>
<p className="text-muted">
You don't have permission to create cards
</p>
</Can>
</div>
);
}
Field-Level Visibility (Advanced):
CASL supports field-level permissions for fine-grained visibility:
// Define field-level permissions in ability
can('read', 'Board', ['name', 'description']); // Can see name and description
can('read', 'Board', ['budget'], { isOwner: true }); // Only owners see budget
// Check field access
const canSeeBudget = ability.can('read', 'Board', 'budget');
// UI implementation
function BoardDetails({ board }: { board: Board }) {
const ability = useAppAbility();
return (
<div>
<h2>{board.name}</h2>
<p>{board.description}</p>
{/* Budget only visible to owners */}
{ability.can('read', subject('Board', board), 'budget') && (
<div>Budget: ${board.budget}</div>
)}
</div>
);
}
Loading State Pattern:
While abilities are loading, show skeleton UI:
function ProtectedPage() {
const { ability, isLoading } = useAbilityWithLoading();
if (isLoading) {
return <SkeletonLoader />;
}
return (
<Can I="read" a="Board" ability={ability}>
<BoardList />
</Can>
);
}
Example visibility logic (CASL version):
// User sees "Kanban" in menu if:
// - Owner/Super Admin (ability.can('manage', 'all') === true), OR
// - Has ANY permission: ability.can('read', 'Board') || ability.can('create', 'Board') || ...
// User sees "Create Board" button if:
// - ability.can('create', 'Board') === true
Deliverables for Phase 5:
In your PRD, include:
<Can> component usage examples for all major actionsability.can() checks for conditional UI<Can not>) for permission denial messagesReference file to consult:
Design RLS-ready schema:
workspaces.idExample schema pattern:
CREATE TABLE kanban_boards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS Policy
CREATE POLICY "Users can access boards in their workspaces"
ON kanban_boards FOR SELECT
TO authenticated
USING (
-- Owner bypass
workspace_id IN (
SELECT id FROM workspaces
WHERE owner_id = auth.uid()
)
OR
-- Super Admin bypass
workspace_id IN (
SELECT organization_id FROM organization_super_admins
WHERE user_id = auth.uid()
)
OR
-- Normal user with permission
user_can(auth.uid(), 'read', 'boards', workspace_id)
);
โก Security Layers: CASL + RLS Coordination
CASL and RLS work together as defense in depth:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ USER INTERACTION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ LAYER 1: CASL (Client-Side Authorization) โ
โ Purpose: UX optimization โ
โ - Shows/hides UI elements โ
โ - Disables buttons for unauthorized actions โ
โ - Prevents unnecessary API calls โ
โ - Improves user experience โ
โ โ
โ Security: โ NOT REAL SECURITY (can be bypassed) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
HTTP Request Sent
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ LAYER 2: RLS (Server-Side Security) โ
โ Purpose: REAL security enforcement โ
โ - Database-level access control โ
โ - Cannot be bypassed by client โ
โ - Enforced even if CASL is disabled/hacked โ
โ - Multi-tenant data isolation โ
โ โ
โ Security: โ
FINAL SOURCE OF TRUTH โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Example Flow:
// ===== CLIENT SIDE =====
// Step 1: User clicks "Delete Board"
function BoardActions({ board }) {
const ability = useAppAbility();
// CASL check: Should we even show the button?
if (!ability.can('delete', 'Board')) {
return null; // Button not rendered
}
async function handleDelete() {
// CASL check passed, send request
const response = await fetch(`/api/boards/${board.id}`, {
method: 'DELETE',
});
// If RLS denies, we get 403 error here
if (!response.ok) {
alert('Permission denied');
}
}
return <button onClick={handleDelete}>Delete</button>;
}
// ===== SERVER SIDE (RLS) =====
-- Step 2: Supabase receives DELETE query
DELETE FROM kanban_boards WHERE id = '...';
-- Step 3: RLS policy checks
-- Policy evaluates:
-- 1. Is user the owner? workspace_id IN (SELECT ...)
-- 2. Is user super admin? workspace_id IN (SELECT ...)
-- 3. Does user have permission? user_can(auth.uid(), 'delete', 'boards', ...)
-- If ANY condition is true โ DELETE succeeds
-- If ALL conditions are false โ ERROR: permission denied
Why Both Layers?
| Scenario | CASL Only | RLS Only | CASL + RLS (Recommended) |
|---|---|---|---|
| Authorized user | โ Button shown | โ DELETE succeeds | โ Button shown, DELETE succeeds |
| Unauthorized user | โ Button hidden | โ DELETE blocked | โ Button hidden, DELETE blocked (if bypassed) |
| Attacker bypasses CASL | โ Button shown via DevTools | โ DELETE blocked | โ DELETE blocked (RLS protects) |
| Performance | โ No unnecessary requests | โ Sends request then fails | โ No unnecessary requests |
| UX | โ Clean UI | โ Buttons shown, fail on click | โ Clean UI |
Architectural Rules:
In Your PRD:
Document both layers:
## Security Architecture
### Client-Side Authorization (CASL)
- Users with `boards.create` permission see "Create Board" button
- Implemented via `<Can I="create" a="Board">`
### Server-Side Security (RLS)
- PostgreSQL policy enforces permission check
- Queries auth.uid() from JWT
- Calls user_can() function for verification
### Sync Strategy
- CASL abilities loaded from same permissions table as RLS
- Permissions cached for 5 minutes in TanStack Query
- Realtime invalidation ensures <500ms sync
Reference file to consult:
Include in your PRD (00-master-prd.md):
Feature Metadata
kanban)productivity)Permission Matrix
resource.action{primary_resource}.read)Role Definitions (if creating default roles)
Special Role Behavior
Workspace Integration
Data Contracts (in entities.ts)
Reference file to consult:
โ Assuming feature inheritance between org and projects โ Each workspace activates features independently
โ Creating "Owner of project" role โ Owner only exists at organization level
โ Hardcoding Owner/Super Admin permissions in permission tables โ Owner/Super Admin use bypass logic, not permission tables
โ Mixing business logic with permissions โ Permissions are pure access control; business rules go in use cases
โ Forgetting workspace_id in entities โ All feature entities must reference workspace
โ Creating permissions without resources โ Always define resources first, then permissions on those resources
โ Ignoring RLS in schema design โ Plan RLS policies during entity design
โ Treating CASL as security โ CASL is UX authorization; RLS is real security
โ Not defining CASL subjects in PRD โ Architect must specify subject mapping for each resource
โ Using different permission formats in CASL vs database
โ
Maintain consistent resource.action format everywhere
โ Forgetting cannot() rules for Super Admin restrictions
โ
Define explicit restrictions: cannot('delete', 'Organization')
โ Not syncing CASL with RLS changes
โ
When RLS policies change, update defineAbilitiesFor() accordingly
โ Hardcoding abilities instead of loading from database โ Load permissions from Supabase, then build CASL abilities dynamically
โ Missing type safety for subjects and actions
โ
Define TypeScript types: type AppAbility = MongoAbility<[Actions, Subjects]>
โ Not handling ability loading states โ Show skeleton UI while abilities are being fetched
Before delivering PRD, verify:
resource.action formatAppAbility type defined with Actions and SubjectsdefineAbilitiesFor() function in entities.tscan('manage', 'all')cannot() rules documentedmapResourceToSubject() helper function included<Can>, useAppAbility)Load these files as needed for detailed information:
When using this skill, structure your PRD with these sections:
## Feature Metadata
- Slug: [feature-slug]
- Category: [category]
- Workspace Scope: [organization|project|both]
- Mandatory: [yes|no]
## Resources and Permissions
### Resources
- [resource-name]: [description]
### Permissions
- [resource].[action]: [description]
### Visibility Permission
- [resource].[action]: Grants UI visibility
## CASL Integration โก
### Subject Mapping
| Database Resource | CASL Subject |
|-------------------|--------------|
| boards | Board |
| cards | Card |
| card_comments | Comment |
### Actions
- Standard: `create`, `read`, `update`, `delete`
- Custom: `move`, `assign` (if applicable)
### Type Definition
```typescript
type Subjects = 'Board' | 'Card' | 'Comment' | 'all';
type Actions = 'create' | 'read' | 'update' | 'delete' | 'move' | 'manage';
export type AppAbility = MongoAbility<[Actions, Subjects]>;
can('manage', 'all')can('manage', 'all') + restrictions<Can I="action" a="Subject">useAppAbility()can('manage', 'all')cannot('delete', 'Organization'), etc.<Can> components and ability.can() checksuser_can() function[Zod schemas + CASL ability builder function]
## Final Note
This system is **highly modular and flexible**. The key is understanding that:
1. **Workspaces are independent** - No sharing, no inheritance
2. **Features are modular** - Can be activated anywhere
3. **Permissions are contextual** - Only apply in specific workspace
4. **Owner/Super Admin are special** - Bypass normal rules with specific restrictions
When in doubt, consult the reference files and remember: **explicit is better than implicit**.