Roles & Permissions
Roles are project-level definitions that carry a set of permission strings. Assign roles to organization members to control what actions they can perform in your application.
Overview
AuthGate uses a flat role-based access control (RBAC) model:
- Roles are defined once per project and reused across all organizations in that project
- Each role has a key (machine-readable), a name (display), and a list of permissions
- Permissions are plain strings in
resource:actionformat — AuthGate stores and checks them; your application defines what they mean - Members of an organization are assigned exactly one role
Project
└── Roles (admin, member, viewer, ...)
└── Permissions (documents:write, org:manage, ...)
Organization
└── Members
└── Role → Permissions
Defining roles
Create and manage roles in the AuthGate dashboard under Settings → Roles, or via the admin API.
Dashboard
Navigate to Settings → Roles → New Role and fill in:
| Field | Description |
|---|---|
| Key | Machine-readable identifier (e.g. admin, member). Immutable after creation. |
| Name | Human-readable label shown in your UI (e.g. "Admin", "Member"). |
| Description | Optional. Explains what this role can do. |
| Permissions | Comma-separated list of permission strings. |
Via the API
Create a role
curl -X POST https://your-authgate-instance.com/api/v1/roles \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"key": "editor",
"name": "Editor",
"description": "Can read and write documents, but cannot manage the organization",
"permissions": ["documents:read", "documents:write", "documents:delete"]
}'
Response
{
"role": {
"id": "role_abc123",
"key": "editor",
"name": "Editor",
"description": "Can read and write documents, but cannot manage the organization",
"permissions": ["documents:read", "documents:write", "documents:delete"],
"is_default": false,
"created_at": "2025-01-15T10:00:00Z"
}
}
Updating a role
Update permissions
curl -X PATCH https://your-authgate-instance.com/api/v1/roles/role_abc123 \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"permissions": ["documents:read", "documents:write", "documents:delete", "comments:write"]
}'
Updating a role's permissions takes effect immediately for all members holding that role across all organizations in the project.
Permission strings
Permissions follow a resource:action format. Both segments are arbitrary strings — choose a convention that maps to your application's domain.
Examples
| Permission | Meaning |
|---|---|
documents:read | Read documents |
documents:write | Create and update documents |
documents:delete | Delete documents |
org:manage | Manage organization settings and members |
billing:view | View billing information |
billing:manage | Change billing plan and payment methods |
api_keys:create | Create API keys |
Naming conventions
- Use lowercase with underscores for multi-word segments:
api_keys:create - Group related permissions under the same resource:
documents:read,documents:write,documents:delete - Prefer specific actions over broad ones — use
org:inviteinstead oforg:manageif you only need to gate invitations
Checking permissions
The @auth-gate/core package exports four utility functions for permission checks. All accept an OrgMembership object as the first argument.
Import utilities
import {
hasPermission,
hasRole,
hasAnyPermission,
hasAllPermissions,
} from "@auth-gate/core";
hasPermission(membership, permission)
Returns true if the membership's role includes the given permission (supports wildcards).
import { rbac } from "../app/rbac";
hasPermission(membership, rbac.permissions.documents.write); // type-safe enum
hasRole(membership, roleKey)
Returns true if the membership's role key matches exactly.
hasRole(membership, rbac.roles.admin); // type-safe enum
hasAnyPermission(membership, permissions)
Returns true if the membership has at least one of the given permissions.
hasAnyPermission(membership, [rbac.permissions.documents.read, rbac.permissions.documents.write]); // full autocomplete ↗
hasAllPermissions(membership, permissions)
Returns true only if the membership has every permission in the list.
hasAllPermissions(membership, [rbac.permissions.billing.view, rbac.permissions.billing.manage]); // full autocomplete ↗
Server-side example
API route guard
import { hasPermission } from "@auth-gate/core";
import { createOrgHelpers } from "@auth-gate/nextjs";
import { rbac } from "../app/rbac";
const { getOrgMembership } = createOrgHelpers({
baseUrl: process.env.AUTHGATE_URL!,
apiKey: process.env.AUTHGATE_API_KEY!,
});
export async function DELETE(
request: Request,
{ params }: { params: { orgId: string } }
) {
const userId = getUserIdFromSession(request);
const membership = await getOrgMembership(params.orgId, userId);
if (!hasPermission(membership, rbac.permissions.org.manage)) { // type-safe enum
return Response.json({ error: "Forbidden" }, { status: 403 });
}
// proceed with deletion
}
React component example
Conditional UI rendering
import { hasPermission } from "@auth-gate/core";
import { useOrganization } from "@auth-gate/react";
import { rbac } from "../app/rbac";
function DocumentActions({ orgId }: { orgId: string }) {
const { membership } = useOrganization(orgId);
if (!membership) return null;
return (
<div>
{hasPermission(membership, rbac.permissions.documents.write) && (
<button>Edit Document</button>
)}
{hasPermission(membership, rbac.permissions.documents.delete) && (
<button>Delete Document</button>
)}
</div>
);
}
Wildcard matching
Permission checks support two wildcard forms:
| Pattern | Matches |
|---|---|
* | Every permission string |
resource:* | Any action on the specified resource |
Wildcard examples
// Role with ["*"] passes all permission checks
hasPermission(adminMembership, "documents:delete"); // true
hasPermission(adminMembership, "billing:manage"); // true
// Role with ["documents:*"] passes checks for documents resource
hasPermission(editorMembership, "documents:read"); // true
hasPermission(editorMembership, "documents:write"); // true
hasPermission(editorMembership, "billing:view"); // false
A common pattern is to give an admin role ["*"] permissions and define granular permissions for other roles:
Example roles
[
{ "key": "admin", "permissions": ["*"] },
{ "key": "editor", "permissions": ["documents:*", "comments:*"] },
{ "key": "viewer", "permissions": ["documents:read", "comments:read"] }
]
Default roles
A default role is automatically assigned to new organization members when no role is specified — for example, when accepting an invitation that doesn't specify a role, or when a member is added programmatically without a roleKey.
Setting the default role
Mark a role as default in the dashboard under Settings → Roles, or via the API:
Set default role
curl -X PATCH https://your-authgate-instance.com/api/v1/roles/role_member \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{ "is_default": true }'
Only one role per project can be the default. Setting a new default clears the previous one automatically.
A typical setup is to make member (with read-only or limited write permissions) the default role, and require an explicit assignment to grant higher privileges.
Project-level roles
By default, roles are assigned to users within an organization. But some applications don't use organizations — or need permissions that apply everywhere regardless of org context. Project-level roles let you assign a role directly to a user at the project level.
When to use project-level roles
- Your app has no concept of organizations — users just sign up and get a role
- You need global permissions (e.g.,
billing:manage) that apply everywhere, even inside orgs - You want to combine both: a site-wide role for global access + org-specific roles for team access
Assigning a project-level role
# Assign a role directly to a user (no org required)
curl -X PUT https://your-project.authgate.dev/api/v1/users/user_123/role \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "roleKey": "premium" }'
# Get a user's project-level role
curl https://your-project.authgate.dev/api/v1/users/user_123/role \
-H "Authorization: Bearer $API_KEY"
# Remove a user's project-level role
curl -X DELETE https://your-project.authgate.dev/api/v1/users/user_123/role \
-H "Authorization: Bearer $API_KEY"
# List all project-level role assignments
curl https://your-project.authgate.dev/api/v1/user-roles \
-H "Authorization: Bearer $API_KEY"
How permissions merge
When a user has both a project-level role and an org-level role, permissions are merged additively (union):
Project role: "premium" → billing:read, billing:manage
Org role: "editor" → documents:read, documents:write
Effective permissions (inside org) → billing:read, billing:manage, documents:read, documents:write
Effective permissions (no org) → billing:read, billing:manage
No role can reduce what the other grants. Project-level permissions always apply, even inside org context.
Checking permissions without an org
// Server-side (Next.js)
const { can } = await ac.getUserRbac(userId); // project-level only
can("billing:manage"); // true
// With org context — permissions are merged
const { can } = await ac.getUserRbac(userId, orgId); // project + org
can("billing:manage"); // true (from project role)
can("documents:write"); // true (from org role)
React
// Without org — project-level only
const { can } = useRbac();
// With org — merged
const { can } = useRbac(orgId);