Skip to main content

Roles & Permissions Blueprint

The roles blueprint ({app}.roles.json) defines every role in your application and the permissions each role holds. Radish compiles this into seeded database records, service-layer authorization checks, and admin panel role management — all from a single JSON file.

Structure​

{
"version": 1,
"roles": {
"ROLE_KEY": {
"label": "Human-Readable Name",
"description": "What this role is for",
"isSystem": true | false,
"permissions": [
"entity:action",
"entity:action:scope"
]
}
}
}

Role Fields​

FieldTypeRequiredDescription
labelstringyesDisplay name shown in the admin panel and API responses
descriptionstringyesExplains the role's purpose — shown in the admin panel's role editor
isSystembooleanyesSystem roles cannot be deleted or renamed from the admin panel. Their permissions can still be edited at runtime.
permissionsstring[]yesList of permission strings this role grants. Can be empty — an empty list means the role exists but has no specific permissions (useful for builtin roles where permissions are added at runtime).

Role Keys​

Role keys are uppercase identifiers (USER, ADMIN, SALES_AGENT). They serve as the stable reference across blueprints, seed data, and code. The key is what gets stored on the user record and checked in authorization logic — the label is purely for display.

Builtin Roles​

Every generated app starts with these roles from builtin.roles.json. You don't need to include them in your roles blueprint — they're merged automatically. You can override their permissions by defining the same key in your blueprint.

KeyLabelSystemPurpose
USERStandard UseryesBase authenticated role. Every registered user has at least this role.
ADMINAdministratoryesFull system access. Admins bypass ownership filters and see all records.
APIAPI AccessyesAssigned to API key sessions. Permissions come from the key's scopes.
ANONYMOUSAnonymousyesUnauthenticated access. Add permissions here to make specific entities publicly readable.
MODERATORModeratornoContent moderation. Included as a common starting point — remove or customize as needed.

Permission Format​

Permissions follow the pattern entity:action or entity:action:scope:

entity:action
entity:action:scope

Entity​

The entity name in lowercase — matching the entity key from your types blueprint. Examples: product, order, customer, blogpost.

Action​

ActionWhat it gates
viewReading records — get(), list(), findById()
createCreating records — create()
editUpdating records — update(), patch()
deleteRemoving records — remove(), archive()

Scope​

Scope is optional and controls which records the permission applies to:

ScopeMeaning
(omitted)Permission applies to the user's own records only (equivalent to :own for owned entities)
:ownExplicitly own-records-only — the user can only operate on records where ownerId matches their user ID
:allAll records regardless of ownership — the user can operate on any record of this entity type

Examples​

"permissions": [
"product:view:all", // Can view ALL products (not just own)
"product:create", // Can create products (owned by them)
"product:edit:own", // Can edit only their own products
"order:view:own", // Can view only their own orders
"customer:view:all", // Can view all customers
"customer:edit:all", // Can edit any customer record
"blogpost:delete:own" // Can delete only their own blog posts
]

How Permissions Are Enforced​

Permissions flow through the four-layer access control model. Each service method checks authorization in this order:

  1. RBAC — does the user's role include the required entity:action permission? If not, the request is rejected with a 403.
  2. Ownership — for entities with "owned": true, is the scope :all or does the record belong to this user? Non-admin users with :own scope only see their own records.
  3. Scoped — in multi-tenant apps, does the record belong to the user's tenant?
  4. Grants — if features.grants is enabled, has another user shared this specific record with the requesting user?

Admins (ADMIN role) bypass ownership and scope checks — they see everything. This is hardcoded in the authorization layer, not permission-driven, so you don't need to add :all permissions to the admin role.

Real-World Patterns​

Simple App — Two Custom Roles​

A task management app where instructors create content and students consume it:

{
"version": 1,
"roles": {
"INSTRUCTOR": {
"label": "Instructor",
"description": "Creates and manages courses, modules, lessons, and assignments",
"isSystem": false,
"permissions": [
"course:create", "course:view:all", "course:edit:own", "course:delete:own",
"module:create", "module:view:all", "module:edit:own", "module:delete:own",
"lesson:create", "lesson:view:all", "lesson:edit:own", "lesson:delete:own",
"assignment:create", "assignment:view:all", "assignment:edit:own",
"submission:view:all", "submission:edit:all"
]
},
"STUDENT": {
"label": "Student",
"description": "Browses courses, enrolls, submits assignments",
"isSystem": false,
"permissions": [
"course:view:all", "module:view:all", "lesson:view:all",
"enrollment:create", "enrollment:view:own",
"assignment:view:all",
"submission:create", "submission:view:own", "submission:edit:own"
]
}
}
}

Note the asymmetry: instructors can view:all submissions (to grade them) while students can only view:own. Instructors can edit:own their courses but not other instructors' courses.

Complex App — Departmental Roles​

A commerce platform with specialized teams:

{
"version": 1,
"roles": {
"ANONYMOUS": {
"label": "Anonymous",
"description": "Unauthenticated storefront browsing",
"isSystem": false,
"permissions": [
"product:view:all", "taxonomy:view:all", "contentpage:view:all"
]
},
"CUSTOMER": {
"label": "Customer",
"description": "Registered customer — browse, purchase, manage account",
"isSystem": false,
"permissions": [
"product:view:all", "cart:view:own", "cart:create", "cart:edit:own",
"order:view:own", "order:create",
"productreview:create", "productreview:view:all", "productreview:edit:own"
]
},
"FULFILLMENT": {
"label": "Fulfillment",
"description": "Warehouse staff — ship orders, manage inventory",
"isSystem": false,
"permissions": [
"order:view:all", "order:edit:all",
"shipment:view:all", "shipment:create", "shipment:edit:all",
"stockitem:view:all", "stockitem:edit:all"
]
},
"CATALOG_MANAGER": {
"label": "Catalog Manager",
"description": "Product team — full product CRUD, pricing rules",
"isSystem": false,
"permissions": [
"product:view:all", "product:create", "product:edit:all", "product:delete:all",
"taxonomy:view:all", "taxonomy:create", "taxonomy:edit:all",
"attribute:view:all", "attribute:create", "attribute:edit:all"
]
}
}
}

Each role sees only what it needs. Fulfillment staff can ship orders but can't edit products. Catalog managers can edit products but can't process orders.

Public Access via ANONYMOUS​

To make entities publicly readable without authentication, add view:all permissions to the ANONYMOUS role:

"ANONYMOUS": {
"label": "Anonymous",
"description": "Unauthenticated access",
"isSystem": false,
"permissions": [
"product:view:all",
"blogpost:view:all",
"contentpage:view:all"
]
}

Without these permissions, unauthenticated requests to those entity endpoints return 401. The ANONYMOUS role is the mechanism for public APIs — there's no separate "public" flag on entities.

Seeding​

Roles are seeded into the database at application startup via radish seed. The seeder:

  1. Reads the compiled roles from the generated code
  2. Creates or updates each role by key (upsert on the role name)
  3. Syncs the permission list — permissions in the blueprint are added; permissions removed from the blueprint are removed from the database
  4. Marks system roles as non-deletable

Roles can also be created and edited at runtime through the admin panel's role management page. Runtime changes persist in the database and survive re-seeding — the seeder only updates roles that exist in the blueprint, it doesn't delete roles that were created at runtime.

Relationship to Entity Ownership​

The roles blueprint works in concert with entity ownership settings in the types blueprint:

Entity ownershipEffect on permissions
"owned": trueRecords have an ownerId. Permissions without :all scope only apply to the user's own records.
"owned": falseNo ownership filtering. Any user with the entity:action permission can operate on any record.
"ownership": "system"System-owned entity (e.g., ScheduledJob). Only admins can access unless explicit permissions are granted.

If an entity is "owned": true and a role has product:view (no scope), the user can only view products they created. Add product:view:all to let them view all products regardless of ownership.

Validation​

radish validate checks the roles blueprint for:

  • Valid JSON structure matching the roles schema
  • Permission format correctness (entity:action or entity:action:scope)
  • Entity references that match entities defined in the types blueprint
  • Action values from the allowed set (view, create, edit, delete)
  • Scope values from the allowed set (own, all)