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 authgate.rbac.ts file in your project root with a sample resource and role definition.

2. Edit the config

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

3. Set environment variables

AUTHGATE_API_KEY=ag_...
AUTHGATE_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:

authgate.rbac.ts

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

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.
  • Routing the config through defineRbac() is what enables type-safe RBAC across your app — the same object written as const without it degrades every permission to string. The export name is not load-bearing: the CLI accepts a default export, a named rbac export, or the module itself.

Wildcard grants

Use "*" as a resource key or action key in grants to avoid listing every permission explicitly. Wildcards are expanded at defineRbac() time against your declared resources and actions, so the resulting grants object is always fully explicit — including the copy rbac sync uploads. A role's grants are therefore fixed by the resources declared now; adding a resource later does not silently widen it.

An action named under the "*" resource applies to every resource that declares it and is skipped for those that do not, so "*": { read: true } is safe alongside a resource with no read action. An action no resource declares is a typo and is rejected.

All resources, all actions

authgate.rbac.ts

export default defineRbac({
  resources: {
    documents: { actions: ["read", "write", "delete"] },
    billing:   { actions: ["read", "manage"] },
  },
  roles: {
    superadmin: {
      name: "Super Admin",
      grants: { "*": { "*": true } },
    },
  },
});

This expands to:

grants: {
  documents: { read: true, write: true, delete: true },
  billing:   { read: true, manage: true },
}

One action across all resources

auditor: {
  name: "Auditor",
  grants: { "*": { read: true } },
},

Expands read: true to every resource that declares a read action. Resources without a read action are skipped.

All actions on one resource

doc_admin: {
  name: "Doc Admin",
  grants: { documents: { "*": true } },
},

Expands to all actions declared on documents (read, write, delete).

Layering: specific overrides wildcard

When a role combines wildcard and specific grants, specifics win:

mostly_reader: {
  name: "Mostly Reader",
  grants: {
    "*":        { read: true },         // base: read on everything
    documents:  { write: true },        // override: also write on documents
  },
},

Expands to:

grants: {
  documents: { read: true, write: true },
  billing:   { read: true },
}

The layering order is:

  1. Wildcard resource grants ("*") provide the base
  2. Specific resource grants override the base
  3. Within a resource, specific action values override a wildcard action ("*")

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

lib/rbac.ts

import { createRbacHooks } from "@auth-gate/react";
import { rbac } from "../authgate.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);  // the string "documents:write"
is(rbac.roles.admin);                   // the role object; is() also accepts "admin"

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

Server helpers (Next.js)

import { createRbacHelpers } from "@auth-gate/nextjs";
import { rbac } from "../authgate.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_URLBase URL of your AuthGate instance (AUTHGATE_BASE_URL is also accepted)

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 authgate.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 authgate.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:

authgate.rbac.ts

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

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:

authgate.rbac.ts

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

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 authgate.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_URL: ${{ secrets.AUTHGATE_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_URL: ${{ secrets.AUTHGATE_URL }}

The dry-run exits with a non-zero code if validation fails. With --strict, a plan that would archive a role with members assigned also exits non-zero, so that case is caught before reaching production. --strict keys off that condition specifically; it is not a general warnings-as-errors switch.

Was this page helpful?