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
| Field | Type | Required | Description |
|---|---|---|---|
label | string | yes | Display name shown in the admin panel and API responses |
description | string | yes | Explains the role's purpose — shown in the admin panel's role editor |
isSystem | boolean | yes | System roles cannot be deleted or renamed from the admin panel. Their permissions can still be edited at runtime. |
permissions | string[] | yes | List 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.
| Key | Label | System | Purpose |
|---|---|---|---|
USER | Standard User | yes | Base authenticated role. Every registered user has at least this role. |
ADMIN | Administrator | yes | Full system access. Admins bypass ownership filters and see all records. |
API | API Access | yes | Assigned to API key sessions. Permissions come from the key's scopes. |
ANONYMOUS | Anonymous | yes | Unauthenticated access. Add permissions here to make specific entities publicly readable. |
MODERATOR | Moderator | no | Content 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
| Action | What it gates |
|---|---|
view | Reading records — get(), list(), findById() |
create | Creating records — create() |
edit | Updating records — update(), patch() |
delete | Removing records — remove(), archive() |
Scope
Scope is optional and controls which records the permission applies to:
| Scope | Meaning |
|---|---|
| (omitted) | Permission applies to the user's own records only (equivalent to :own for owned entities) |
:own | Explicitly own-records-only — the user can only operate on records where ownerId matches their user ID |
:all | All 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:
- RBAC — does the user's role include the required
entity:actionpermission? If not, the request is rejected with a 403. - Ownership — for entities with
"owned": true, is the scope:allor does the record belong to this user? Non-admin users with:ownscope only see their own records. - Scoped — in multi-tenant apps, does the record belong to the user's tenant?
- Grants — if
features.grantsis 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:
- Reads the compiled roles from the generated code
- Creates or updates each role by key (upsert on the role name)
- Syncs the permission list — permissions in the blueprint are added; permissions removed from the blueprint are removed from the database
- 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 ownership | Effect on permissions |
|---|---|
"owned": true | Records have an ownerId. Permissions without :all scope only apply to the user's own records. |
"owned": false | No 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:actionorentity: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)
Related Documentation
- Entity Definitions — defining entities and their ownership model
- Four-Layer Access Control — how permissions compose with ownership, scoping, and grants
- Services — how the service layer enforces permissions
- App Blueprint — audience personas that inform role design