Skip to main content

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.

Built-in roles

Each tenant has three built-in roles. Permissions are additive — Owner has all permissions.
PermissionOwnerAdminMember
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:
[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:
// 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.
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:
PATCH /api/v1/tenants/current/members/{userId}/role
Authorization: Bearer <token>

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

Permission propagation timing

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.

RBAC in the frontend

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

if (hasPermission(Permissions.TenantMembersInvite)) {
  // show invite button
}
PermissionGate component — wraps content that should only render for users with a specific permission:
<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 for details.

How to add a new permission

See Customizing Roles for the step-by-step guide.