Chapter 4 — permissions
Define roles and gate the UI by role.
This chapter adds two roles — admin and agent. Admins can delete contacts; agents cannot.
Seed roles
Roles live in the actor_roles table. The standards SDK does not expose a code-level createRole() helper; define them in Supabase migrations instead.
-- supabase/migrations/0010_roles.sql
-- Insert role definitions
INSERT INTO roles (tenant_id, name, label) VALUES
('<your-tenant-id>', 'admin', 'Admin'),
('<your-tenant-id>', 'agent', 'Agent');
-- Grant the admin role to your founder account
INSERT INTO actor_roles (actor_id, role)
SELECT id, 'admin' FROM actors WHERE type = 'user' AND email = 'founder@example.com';After the migration runs, assign the agent role to your team members the same way, or use the People settings UI in your standards workspace.
See the RBAC concept page for the full role model: how permissions are scoped (object vs. system), how they compose across multiple role assignments, and the wildcard "*" target.
Protect the API
Attach TenantAuthGuard and PermissionGuard from @stndrds/adapter-nestjs to your controller, then declare the required permission with @RequirePermission:
// @noverify
import { Controller, Delete, Param, UseGuards } from "@nestjs/common";
import {
PermissionGuard,
RequirePermission,
TenantAuthGuard,
} from "@stndrds/adapter-nestjs";
@Controller("contacts")
@UseGuards(TenantAuthGuard, PermissionGuard)
export class ContactsController {
@RequirePermission({ objectName: "contacts", action: "delete" })
@Delete(":id")
async remove(@Param("id") id: string) {
// only reached when the caller has delete on "contacts"
}
}PermissionGuard reads the current actor from the request context (set by TenantAuthGuard), resolves their effective permissions, and throws 403 Forbidden if the required action is missing. No permission required on a route means all authenticated users can reach it.
Always place TenantAuthGuard before PermissionGuard in the @UseGuards() list. PermissionGuard depends on the actor ID that TenantAuthGuard injects into the request context.
For the complete guard setup with JWT verification and API-key auth, see the NestJS backend guide.
Gate the UI
Use useCanAccess from @stndrds/react to hide actions the current user cannot perform. It returns undefined while loading, then true or false.
// @noverify
import { useCanAccess } from "@stndrds/react";
function DeleteButton({ onDelete }: { onDelete: () => void }) {
const canDelete = useCanAccess("contacts", "delete");
if (!canDelete) return null;
return <button onClick={onDelete}>Delete</button>;
}useCanAccess calls useObjectPermissions internally and maps the action to the canDelete boolean on ObjectPermissions. It returns false (not undefined) when the user lacks the permission, so the null branch covers both the loading state and the denied state.
When you need finer control — for example, disabling a button rather than hiding it — use useObjectPermissions("contacts") directly and read perms?.canDelete.
The CRM is now functional: objects are defined, the backend is wired, views are mounted, and access is gated by role. Clone the example skeleton at apps/docs/examples/crm-tutorial/ for a complete reference covering all four chapters.