Codapult uses a role-based access control (RBAC) system to manage what users can do within organizations.
Roles
Every organization member has one of four roles, ordered from most to least privileged:
| Role | Level | Capabilities |
|---|---|---|
| Owner | 4 | Full control — delete org, manage billing, all actions |
| Admin | 3 | Manage members, settings, billing, resources |
| Member | 2 | Create and edit resources, view billing |
| Viewer | 1 | Read-only access to org resources and settings |
Global admins (user.role === 'admin') bypass all organization-level permission checks.
Permission Definitions
Permissions are defined in src/lib/permissions.ts as a map of action to allowed roles:
const permissions = {
'org:update': ['owner', 'admin'],
'org:delete': ['owner'],
'member:invite': ['owner', 'admin'],
'member:remove': ['owner', 'admin'],
'member:update-role': ['owner', 'admin'],
'member:list': ['owner', 'admin', 'member', 'viewer'],
'billing:manage': ['owner', 'admin'],
'billing:view': ['owner', 'admin', 'member'],
'resource:create': ['owner', 'admin', 'member'],
'resource:read': ['owner', 'admin', 'member', 'viewer'],
'resource:update': ['owner', 'admin', 'member'],
'resource:delete': ['owner', 'admin'],
'settings:manage': ['owner', 'admin'],
'invitation:create': ['owner', 'admin'],
'invitation:revoke': ['owner', 'admin'],
} satisfies Record<string, readonly OrgRole[]>;
Helper Functions
import { hasPermission, isRoleAtLeast, canModifyRole } from '@/lib/permissions';
hasPermission('member', 'resource:create'); // true
hasPermission('viewer', 'resource:create'); // false
isRoleAtLeast('admin', 'member'); // true
canModifyRole('admin', 'owner'); // false (must be strictly higher)
Server-Side Guards
Use guards from src/lib/guards.ts to enforce permissions in server actions and API routes:
| Guard | Description |
|---|---|
requireAuth() | User is logged in — throws if not |
requireOrgMembership(orgId) | User is a member of the organization |
requireOrgAdmin(orgId) | User is an owner or admin |
requireOrgPermission(orgId, permission) | User has a specific permission in the organization |
All org guards return an OrgAuthContext with the session, orgId, and the user's role.
In Server Actions
'use server';
import { requireOrgPermission } from '@/lib/guards';
export async function updateOrgSettings(orgId: string, data: unknown) {
const { session, role } = await requireOrgPermission(orgId, 'settings:manage');
// Only owners and admins reach this point
}
In API Routes
import { requireOrgMembership } from '@/lib/guards';
export async function GET(req: Request) {
const orgId = req.headers.get('x-org-id');
if (!orgId) return NextResponse.json({ error: 'Missing org' }, { status: 400 });
const { session } = await requireOrgMembership(orgId);
// Return org data...
}
Adding Custom Permissions
- Add the permission key to the
permissionsmap insrc/lib/permissions.ts:
'project:archive': ['owner', 'admin'],
- Use it in your server action or API route:
await requireOrgPermission(orgId, 'project:archive');
- Optionally check it on the client for conditional UI rendering:
import { hasPermission } from '@/lib/permissions';
const canArchive = hasPermission(currentRole, 'project:archive');
Checking Permissions in UI
For conditional rendering based on the current user's role, pass the role from the server and use hasPermission on the client:
{
hasPermission(role, 'member:invite') && <InviteMemberButton orgId={orgId} />;
}