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:action format — 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:

FieldDescription
KeyMachine-readable identifier (e.g. admin, member). Immutable after creation.
NameHuman-readable label shown in your UI (e.g. "Admin", "Member").
DescriptionOptional. Explains what this role can do.
PermissionsComma-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"]
  }'

Permission strings

Permissions follow a resource:action format. Both segments are arbitrary strings — choose a convention that maps to your application's domain.

Examples

PermissionMeaning
documents:readRead documents
documents:writeCreate and update documents
documents:deleteDelete documents
org:manageManage organization settings and members
billing:viewView billing information
billing:manageChange billing plan and payment methods
api_keys:createCreate 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:invite instead of org:manage if 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:

PatternMatches
*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 }'

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);

Was this page helpful?