Skip to content

Permissions

CruzJS uses a resource-based permission system where permissions follow the format resource:action. Roles are mapped to sets of permissions, and access is enforced using requirePermission middleware in routers and loaders.

Permissions use a resource:action naming convention:

type Permission =
| 'org:read'
| 'org:write'
| 'org:delete'
| 'member:read'
| 'member:write'
| 'member:delete'
| 'billing:read'
| 'billing:write'
| 'pipeline:read'
| 'pipeline:write'
| 'pipeline:delete';

Each role has a defined set of permissions. The OWNER role has a special wildcard (*) that grants all permissions:

import { ALL_PERMISSIONS } from '@cruzjs/core/orgs/org.models';
export const rolePermissions: Record<OrgRole, Permission[] | typeof ALL_PERMISSIONS> = {
OWNER: ALL_PERMISSIONS, // '*' -- every permission
ADMIN: [
'org:read', 'org:write',
'member:read', 'member:write', 'member:delete',
'billing:read', 'billing:write',
'pipeline:read', 'pipeline:write', 'pipeline:delete',
],
MEMBER: [
'org:read',
'member:read',
'pipeline:read', 'pipeline:write',
],
VIEWER: [
'org:read',
'pipeline:read',
],
};

Use the requirePermission middleware in route loaders and actions:

import { requirePermission, requireAnyPermission } from '@cruzjs/start/orgs/auth.utils';
import { requireSession } from '@cruzjs/core/shared/middleware/session.middleware';
import { requireOrgContext } from '@cruzjs/core/shared/middleware/org-context.middleware';
export async function loader({ request, params }: LoaderFunctionArgs) {
const auth = await requireSession(request);
const orgContext = await requireOrgContext(request, params, auth);
// Require a single permission
await requirePermission(orgContext, 'billing:read');
// Or require any of several permissions
await requireAnyPermission(orgContext, ['member:write', 'member:delete']);
// User has permission -- proceed with the loader
return { data: await fetchData(orgContext.org.orgId) };
}

If the user lacks the required permission, a 403 Forbidden response is thrown:

{
"error": {
"code": "FORBIDDEN",
"message": "Permission denied: billing:read"
}
}

Use requirePermission inside tRPC procedures. It throws a FORBIDDEN error automatically if the user lacks the permission:

import { orgProcedure, router } from '@cruzjs/core/trpc/context';
import { requirePermission } from '@cruzjs/core/shared/middleware/permission.middleware';
import { getAppContainer } from '@cruzjs/core';
import { BillingService } from './billing.service';
export const billingRouter = router({
getPlans: orgProcedure.query(async ({ ctx }) => {
await requirePermission(ctx, 'billing:read');
const container = await getAppContainer();
const service = container.resolve(BillingService);
return service.getPlans();
}),
});

To add custom permissions for your application, extend the Permission type and update the role mapping:

// In your app's types
import type { Permission as BasePermission } from '@cruzjs/core/orgs/org.models';
// Extend with your custom permissions
type AppPermission = BasePermission
| 'project:read'
| 'project:write'
| 'project:delete'
| 'report:read'
| 'report:export';
// Create your custom role mapping
const appRolePermissions: Record<OrgRole, AppPermission[] | '*'> = {
OWNER: '*',
ADMIN: [
// Include base permissions
'org:read', 'org:write',
'member:read', 'member:write', 'member:delete',
'billing:read', 'billing:write',
// Add custom permissions
'project:read', 'project:write', 'project:delete',
'report:read', 'report:export',
],
MEMBER: [
'org:read', 'member:read',
'project:read', 'project:write',
'report:read',
],
VIEWER: [
'org:read',
'project:read',
'report:read',
],
};

Check permissions on the client side to conditionally render UI elements:

// In your React component
function ProjectSettings({ orgId, userRole }: Props) {
// Simple role-based check
const canEdit = userRole === 'OWNER' || userRole === 'ADMIN';
return (
<div>
<h2>Project Settings</h2>
{canEdit ? (
<button onClick={handleSave}>Save Changes</button>
) : (
<p>You do not have permission to edit settings.</p>
)}
</div>
);
}

For more granular checks, derive permissions from the user’s role on the server and pass them to the client:

// In your loader
export async function loader({ request, params }: LoaderFunctionArgs) {
const auth = await requireSession(request);
const orgContext = await requireOrgContext(request, params, auth);
const role = orgContext.org.role;
return {
permissions: {
canEditProject: role === 'OWNER' || role === 'ADMIN' || role === 'MEMBER',
canDeleteProject: role === 'OWNER' || role === 'ADMIN',
},
};
}