Permissions & RBAC

Nebutra's role-based access control model — 4 roles, 17 typed scopes, and how to enforce them.

Overview

Nebutra ships a production-ready RBAC (Role-Based Access Control) system out of the box. It includes:

  • 4 built-in roles: OWNER, ADMIN, MEMBER, VIEWER
  • 17 typed permission scopes across resources
  • In-process enforcement via CASL (sub-millisecond)
  • Zanzibar-scale enforcement via OpenFGA (optional, for complex policy graphs)
  • React UI gates via the <Can> component

Roles

RoleDescription
OWNERFull control — billing, deletion, all scopes
ADMINManage team, settings, API keys — all scopes except org deletion
MEMBERCore product access — create, read, update
VIEWERRead-only access

Each tenant member has exactly one role. Roles cannot be combined.

Scope reference

ScopeDescriptionOWNERADMINMEMBERVIEWER
org:readRead org details
org:updateUpdate org settings
org:deleteDelete the organization
member:readList org members
member:inviteInvite new members
member:removeRemove a member
member:update_roleChange a member's role
api_key:createCreate API keys
api_key:readList API keys (masked)
api_key:deleteRevoke API keys
project:createCreate new projects
project:readRead project data
project:updateUpdate project settings
project:deleteDelete a project
billing:readView billing info
billing:updateChange plan / payment
audit_log:readView audit log

Enforcing permissions

API Gateway (Hono middleware)

import { requirePermission } from "@nebutra/permissions";

// Protects the route — returns 403 if the scope is missing
app.delete("/api/projects/:id", requirePermission("project:delete"), handler);

Server Actions (Next.js)

"use server";
import { requirePermission } from "@nebutra/permissions";

export async function deleteProject(projectId: string) {
  await requirePermission("project:delete"); // throws if unauthorized
  // ...
}

React UI gates

import { Can } from "@nebutra/permissions/react";

// Only renders children if the user has the scope
<Can action="delete" resource="Project">
  <DeleteButton />
</Can>

// Inverse — show a fallback for users without the scope
<Can action="billing:update" fallback={<UpgradePrompt />}>
  <BillingSettings />
</Can>

Manual check

import { can } from "@nebutra/permissions";

const allowed = await can(userId, "project:create", { orgId });

if (!allowed) {
  return new Response("Forbidden", { status: 403 });
}

Custom scopes

You can extend the scope list for your own resources:

// packages/iam/permissions/src/scopes.ts
export const SCOPES = [
  // built-in scopes...
  "report:generate",  // custom scope
  "widget:embed",     // custom scope
] as const;

Then add them to the role matrix in the same file.

OpenFGA (optional)

For complex authorization scenarios — resource sharing, cross-tenant policies, or graph-based access — Nebutra supports OpenFGA as a drop-in replacement:

PERMISSIONS_PROVIDER=openfga
OPENFGA_API_URL=https://api.us1.fga.dev
OPENFGA_STORE_ID=your_store_id
OPENFGA_API_KEY=your_api_key

The application code doesn't change — requirePermission() and <Can> work identically with both backends.

How is this guide?

Edit on GitHub

Last updated on

On this page