> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Roles and Permissions

> Three built-in roles, ten granular permissions, dynamic policy enforcement, and custom role creation.

## Built-in roles

Each tenant has three built-in roles. Permissions are additive — Owner has all permissions.

| Permission                  | Owner | Admin | Member |
| --------------------------- | :---: | :---: | :----: |
| `tenant.settings.read`      |   ✓   |   ✓   |    ✓   |
| `tenant.settings.edit`      |   ✓   |   ✓   |    ✗   |
| `tenant.members.read`       |   ✓   |   ✓   |    ✓   |
| `tenant.members.invite`     |   ✓   |   ✓   |    ✗   |
| `tenant.members.remove`     |   ✓   |   ✓   |    ✗   |
| `tenant.billing.read`       |   ✓   |   ✓   |    ✗   |
| `tenant.billing.manage`     |   ✓   |   ✗   |    ✗   |
| `tenant.roles.read`         |   ✓   |   ✓   |    ✗   |
| `tenant.roles.manage`       |   ✓   |   ✗   |    ✗   |
| `tenant.ownership.transfer` |   ✓   |   ✗   |    ✗   |

Built-in role names (`Owner`, `Admin`, `Member`) are reserved. You cannot create a custom role with those names.

## How permissions are enforced on the backend

Controllers use the `[HasPermission]` attribute:

```csharp theme={null}
[HttpPost("current/invitations")]
[HasPermission(Permissions.TenantMembersInvite)]
public async Task<IActionResult> InviteMember([FromBody] InviteMemberRequest request)
{
    var command = new InviteMemberCommand(request.Email, request.RoleId);
    var result = await _mediator.Send(command);
    return result.ToActionResult();
}
```

The `HasPermission` attribute is backed by a dynamic ASP.NET Core authorization policy provider. There is no startup registration of policies. The provider creates a policy for any permission string on demand:

```csharp theme={null}
// DynamicPermissionPolicyProvider.cs
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
    if (policyName.StartsWith("Permission:"))
    {
        var permission = policyName["Permission:".Length..];
        var policy = new AuthorizationPolicyBuilder()
            .RequireClaim("permissions", permission)
            .Build();
        return Task.FromResult<AuthorizationPolicy?>(policy);
    }
    return _fallback.GetPolicyAsync(policyName);
}
```

Permissions come from JWT claims, not from a database query on every request. When a user's role is changed or a role's permissions are updated, the change takes effect when the user's access token expires (up to 15 minutes later) and they receive a new one via token refresh.

## Custom roles

Any tenant member with the `tenant.roles.manage` permission can create custom roles. Custom roles have a configurable subset of the ten available permissions.

```http theme={null}
POST /api/v1/tenants/current/roles
Authorization: Bearer <token>

{
  "name": "Developer",
  "permissions": [
    "tenant.settings.read",
    "tenant.members.read",
    "tenant.billing.read"
  ]
}
```

Constraints:

* Role name must be unique within the tenant
* Role name cannot be `Owner`, `Admin`, or `Member` (reserved)
* At least one permission must be selected
* Permission strings must be valid (unknown strings are rejected)

Assign a custom role to a member:

```http theme={null}
PATCH /api/v1/tenants/current/members/{userId}/role
Authorization: Bearer <token>

{ "roleId": "<custom-role-id>" }
```

## Permission propagation timing

<Warning>
  Permissions are encoded in the JWT access token at the time it is issued. If you change a role's permissions, users currently logged in with that role will continue using the old permissions until their access token expires (15 minutes maximum) and they refresh.

  For permission removals that need to take effect immediately, the only option is to force re-authentication. There is no built-in revocation mechanism for access tokens — this is a known tradeoff of the stateless JWT approach.
</Warning>

## RBAC in the frontend

**`usePermissions()` hook** — returns an object of `{ hasPermission: (permission: string) => boolean }`. Reads from the decoded JWT in the Zustand auth store.

```typescript theme={null}
const { hasPermission } = usePermissions();

if (hasPermission(Permissions.TenantMembersInvite)) {
  // show invite button
}
```

**`PermissionGate` component** — wraps content that should only render for users with a specific permission:

```tsx theme={null}
<PermissionGate permission={Permissions.TenantBillingManage}>
  <ManageBillingButton />
</PermissionGate>
```

**Command palette** — each command in the command palette specifies a `requiredPermission`. Commands the user does not have permission for are not shown. See [Command Palette](/guides/command-palette) for details.

## How to add a new permission

See [Customizing Roles](/guides/customizing-roles) for the step-by-step guide.
