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
truefor boolean access (Phase 1). Actions not listed in a role's grants are denied by default. - The named
rbacexport 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 ↗
The standalone hooks (useRbac, etc.) are still exported from @auth-gate/react with string parameters for backward compatibility. The factory is the upgrade path for type safety.
Environment variables
| Variable | Description |
|---|---|
AUTHGATE_API_KEY | Your project API key (ag_...) |
AUTHGATE_BASE_URL | Base URL of your AuthGate instance |
CLI commands
| Command | Description |
|---|---|
npx @auth-gate/rbac sync | Dry-run — print the diff without applying |
npx @auth-gate/rbac sync --apply | Apply the diff to AuthGate |
npx @auth-gate/rbac sync --apply --force | Apply including archiving roles with assigned members |
npx @auth-gate/rbac sync --strict | Treat warnings as errors (recommended for CI/CD) |
npx @auth-gate/rbac init | Generate a starter app/rbac.ts config |
npx @auth-gate/rbac check | Validate config locally (no server round-trip) |
Without --apply, sync is always safe to run. Use it in pull request checks to surface role changes for review before they reach production. Add --strict in CI/CD to fail on any warnings.
How sync works
When you run npx @auth-gate/rbac sync, the CLI:
- Loads and validates your
app/rbac.tsconfig - Fetches the current resource and role state from the AuthGate server
- Computes a diff: which resources and roles to create, update, or archive
- In dry-run mode: prints the diff to stdout and exits
- 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.
Archiving a role with assigned members requires the --force flag. Without it, the CLI will print a warning and skip the archive step to protect existing member assignments.
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.
renamedFrom is a one-time directive. After the rename is applied, you can remove it from the config. If you leave it in, the sync engine will warn that no matching role was found and proceed normally.
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
billingandmembersgrants
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.tsappear 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
syncand 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.