RBAC as Code

Define your resources, roles, and permissions in a TypeScript config file and sync them to AuthGate with a single CLI command. RBAC as code means your RBAC configuration is version-controlled, reviewable in pull requests, and deployable through your existing CI/CD pipeline.


Installation

npm install @auth-gate/rbac

Quick start

1. Generate a starter config

npx @auth-gate/rbac init

This creates an app/rbac.ts file in your project root with a sample resource and role definition.

2. Edit the config

Open app/rbac.ts and define your resources, roles, and grants. See Config format below.

3. Set environment variables

AUTHGATE_API_KEY=ag_...
AUTHGATE_BASE_URL=https://...

4. Preview changes

npx @auth-gate/rbac sync

Runs a dry-run and prints a diff of what will be created, updated, or archived. No changes are applied.

5. Apply changes

npx @auth-gate/rbac sync --apply

Executes the diff against AuthGate.


Config format

Use defineRbac() to declare your resources and roles:

app/rbac.ts

import { defineRbac } from "@auth-gate/rbac";

export const rbac = defineRbac({
  resources: {
    documents: { actions: ["read", "write", "delete", "share"] },
    billing:   { actions: ["read", "manage"] },
    members:   { actions: ["invite", "remove", "update_role"] },
  },
  roles: {
    admin: {
      name: "Administrator",
      grants: {
        documents: { read: true, write: true, delete: true, share: true },
        billing:   { read: true, manage: true },
        members:   { invite: true, remove: true, update_role: true },
      },
    },
    editor: {
      name: "Editor",
      grants: {
        documents: { read: true, write: true },
        billing:   { read: true },
      },
    },
    viewer: {
      name: "Viewer",
      grants: {
        documents: { read: true },
      },
    },
  },
});

// Default export for CLI compatibility
export default rbac;

Key points about the config:

  • Resource keys (documents, billing, members) are stable identifiers used internally. Each resource declares its available actions as a string array.
  • Role keys (admin, editor, viewer) are stable identifiers. If you need to rename a key after syncing, use renamedFrom to preserve existing member assignments.
  • Grants map resources to their permitted actions. Each grant value is true for boolean access (Phase 1). Actions not listed in a role's grants are denied by default.
  • The named rbac export enables type-safe RBAC across your app.

Type-safe RBAC

defineRbac() returns a typed object that carries your resource, role, and action keys as literal types. Pass it to factory functions to get full IDE autocomplete everywhere — like tRPC, but for access control.

React hooks

app/rbac.ts

import { createRbacHooks } from "@auth-gate/react";
import { rbac } from "../app/rbac";

export const { useRbac, RbacGate } = createRbacHooks(rbac);

Now all resource/role/action parameters are typed:

const { can, is } = useRbac(orgId);
can(rbac.permissions.documents.write);  // type-safe enum
is(rbac.roles.admin);                   // type-safe enum

<RbacGate orgId={orgId} permission={rbac.permissions.billing.manage}>
  <BillingSettings />
</RbacGate>

Server helpers (Next.js)

import { createRbacHelpers } from "@auth-gate/nextjs";
import { rbac } from "../app/rbac";

const helpers = createRbacHelpers({
  rbac,
  baseUrl: process.env.AUTHGATE_URL!,
  apiKey: process.env.AUTHGATE_API_KEY!,
});

await helpers.checkPermission(userId, orgId, rbac.permissions.documents.write);  // full autocomplete ↗
await helpers.checkRole(userId, orgId, rbac.roles.admin);                        // full autocomplete ↗

Environment variables

VariableDescription
AUTHGATE_API_KEYYour project API key (ag_...)
AUTHGATE_BASE_URLBase URL of your AuthGate instance

CLI commands

CommandDescription
npx @auth-gate/rbac syncDry-run — print the diff without applying
npx @auth-gate/rbac sync --applyApply the diff to AuthGate
npx @auth-gate/rbac sync --apply --forceApply including archiving roles with assigned members
npx @auth-gate/rbac sync --strictTreat warnings as errors (recommended for CI/CD)
npx @auth-gate/rbac initGenerate a starter app/rbac.ts config
npx @auth-gate/rbac checkValidate config locally (no server round-trip)

How sync works

When you run npx @auth-gate/rbac sync, the CLI:

  1. Loads and validates your app/rbac.ts config
  2. Fetches the current resource and role state from the AuthGate server
  3. Computes a diff: which resources and roles to create, update, or archive
  4. In dry-run mode: prints the diff to stdout and exits
  5. With --apply: executes the changes against the AuthGate database

Resources and roles are matched by their config key (admin, editor, etc.). If a key is present in the config but not on the server, it is created. If it exists on both sides, it is updated. If it exists on the server but is absent from the config, it is archived.


Renaming roles

If you need to change a role's config key (e.g., admin to org_admin), use renamedFrom to tell the sync engine that this is a rename rather than an archive-and-create:

app/rbac.ts

import { defineRbac } from "@auth-gate/rbac";

export default defineRbac({
  resources: {
    documents: { actions: ["read", "write", "delete", "share"] },
  },
  roles: {
    org_admin: {
      name: "Organization Admin",
      renamedFrom: "admin",
      grants: {
        documents: { read: true, write: true, delete: true, share: true },
      },
    },
  },
});

When the sync engine sees renamedFrom: "admin", it updates the existing role's config key from "admin" to "org_admin" instead of archiving one and creating the other. All existing member assignments are preserved — the underlying role ID stays the same.

The old key is stored in a previousConfigKeys array, so chain renames (admin -> org_admin -> organization_admin) work correctly across multiple syncs.


Role inheritance

Roles can inherit permissions from other roles using the inherits field. This avoids duplication and makes the permission hierarchy explicit:

app/rbac.ts

import { defineRbac } from "@auth-gate/rbac";

export default defineRbac({
  resources: {
    documents: { actions: ["read", "write", "delete", "share"] },
    billing:   { actions: ["read", "manage"] },
    members:   { actions: ["invite", "remove", "update_role"] },
  },
  roles: {
    viewer: {
      name: "Viewer",
      grants: {
        documents: { read: true },
      },
    },
    editor: {
      name: "Editor",
      inherits: ["viewer"],
      grants: {
        documents: { write: true },
      },
    },
    admin: {
      name: "Admin",
      inherits: ["editor"],
      grants: {
        documents: { delete: true, share: true },
        billing:   { read: true, manage: true },
        members:   { invite: true, remove: true, update_role: true },
      },
    },
  },
});

Inheritance follows an additive-only model: a child role receives all permissions from its parents plus its own grants. In the example above:

  • viewer can documents:read
  • editor inherits from viewer, so it can documents:read + documents:write
  • admin inherits from editor (and transitively from viewer), so it gets all document permissions plus its own billing and members grants

Permissions are never removed through inheritance — a child role always has at least the permissions of its parents.


Coexistence with the dashboard

Config-managed roles and dashboard-managed roles can coexist in the same project:

  • Roles synced from app/rbac.ts appear as read-only in the AuthGate dashboard. You cannot edit them through the UI — all changes must go through the config file.
  • Roles created directly in the dashboard are unaffected by sync and remain fully editable in the UI.
  • Both types of roles are available for assignment and appear in the organization roles API.

Project-level roles

Role definitions in your defineRbac() config are scope-agnostic — the same role can be assigned at the organization level or directly to a user at the project level. You don't need to change your config to support project-level roles.

See Roles & Permissions — Project-level roles for assignment and permission merging details.

The type-safe helpers work with both modes:

// Without org — project-level permissions only
const { can, is } = await ac.getUserRbac(userId);
can(rbac.permissions.billing.manage);  // checks project role

// With org — merged permissions
const { can, is } = await ac.getUserRbac(userId, orgId);
can(rbac.permissions.billing.manage);   // project role ✓
can(rbac.permissions.documents.write);  // org role ✓

CI/CD integration

Add role syncing to your deployment pipeline so RBAC configuration is always in sync with your code:

.github/workflows/deploy.yml

- name: Sync RBAC roles
  run: npx @auth-gate/rbac sync --apply
  env:
    AUTHGATE_API_KEY: ${{ secrets.AUTHGATE_API_KEY }}
    AUTHGATE_BASE_URL: ${{ secrets.AUTHGATE_BASE_URL }}

Run the dry-run with --strict in pull request checks to catch warnings early:

.github/workflows/pr.yml

- name: Preview RBAC role changes
  run: npx @auth-gate/rbac sync --strict
  env:
    AUTHGATE_API_KEY: ${{ secrets.AUTHGATE_API_KEY }}
    AUTHGATE_BASE_URL: ${{ secrets.AUTHGATE_BASE_URL }}

The dry-run exits with a non-zero code if validation fails. With --strict, any warnings (stale renamedFrom, roles with assigned members being archived, etc.) also cause a non-zero exit, so issues are caught before reaching production.

Was this page helpful?