filipebraida/adonisjs-starter-kit/authorization
Claude Code and agent skill by filipebraida/adonisjs-starter-kit
- community
Checking that the SKILL.md file is still in the repository.
RBAC + Bouncer pattern in an AdonisJS + Inertia app. Capabilities live in a `PERMISSIONS` const, role slugs in `ROLES`; a `WithRoles` mixin composes into `User` to expose `hasPermission` / `hasRole` / `syncRoles`; `BasePolicy` subclasses gate routes; the frontend consumes a typed `useCan()` prop. Escalation is protected in depth. Use when adding a capability, gating a route, adding a policy, extending role logic, or reading `can` on the frontend. Trigger on: "add permission", "gate route", "policy", "useCan", "role", "RBAC".
Install
npx vskill@latest install filipebraida/adonisjs-starter-kit/authorizationSKILL.md
Authorization
Role-based access control implemented in plain code on top of AdonisJS Bouncer — no external RBAC library. Four moving parts:
- `PERMISSIONS` — a const catalog of every capability the app understands.
- `ROLES` — a const catalog of role slugs, each with a weight used to pick a display role.
- `WithRoles` — a mixin composed into
Userthat grantsassignRole/hasRole/hasPermission/syncRoles. - `BasePolicy` subclasses — enforce
hasPermission(...)at the controller.
On the frontend a typed GlobalPermissions object flows through Inertia's shared props and is consumed via useCan(). Adding a permission is a two-line change (PERMISSIONS const + wherever it's gated); adding a role is a config + seed touch.
Rules
- Single source of truth for capabilities in a
PERMISSIONSconst object under theusersmodule. Naming:<mod>.<action>(users.create,tokens.view_list). Adding a permission = adding one entry + referencing it wherever it's gated. A new module's capabilities go in this same const — the<mod>.<action>naming already namespaces them; don't fork a secondPERMISSIONS. One central catalog; the policies that consume it stay in each module. - Role slugs live in a
ROLESconst with a matchingROLE_WEIGHTSmap. The weights drivemainRole()for display ordering. Adding a role = new entry + a seed that grants its permissions. - Roles store their own permissions on the
Rolemodel as a JSON column (role.permissions: Permission[]). Seed withRole.updateOrCreate({ name }, { permissions: [...] }). - `WithRoles` mixin composes into
Usernext towithAuthFinder. It exposes: - Mutations:
assignRole(role),assignRoles(roles),syncRoles(roles),revokeRole(role),revokeRoles(roles). - Checks:
hasRole(name),hasPermission(permission). - Reads:
getRoleNames(),getPermissions(),preloadedRoles(sync read of.load('roles')). - Policies subclass
BasePolicyand live atapp/<mod>/policies/<entity>_policy.ts. One method per gated action. Signature:currentUserfirst, resource second when applicable. Returnboolean | AuthorizerResponse. - Gate at the controller before validating:
await bouncer.with(EntityPolicy).authorize('methodName', resource?). On deny it throws → 403 (JSON) or 302 back (HTML/Inertia). Every mutation route (POST/PUT/PATCH/DELETE) must be gated by a Bouncer policy — no exceptions. - Frontend shared prop: an Inertia middleware computes a small
can: GlobalPermissionsobject (manageUsers,manageTokens, …) and shares it on every request. Read withuseCan(). Extending the object = adding a boolean field toglobal_permissions.tscomputed fromuser.hasPermission(PERMISSIONS.x);useCantypes update automatically. - Escalation guards are defense-in-depth on top of validators:
requireManageRoles(executor)— throws if the executor lacksusers.manage_roles.requireManageRolesIfEscalating(executor, desiredRoles)— enforces the check even when a validator accepted a non-default role. Call at create / invite paths.AdminLockoutException— prevents a user from removing their ownadminrole. Raised insideSyncUserRoleswhen target and executor are the same and admin is being dropped.
Repo refs
- Capabilities + roles:
app/users/enums/permission.ts,app/users/enums/role.ts. WithRolesmixin:app/users/mixins/with_roles.ts. Policy:app/users/policies/user_policy.ts.- Escalation guards:
app/users/actions/sync_user_roles.ts. - Shared
canprop → frontenduseCan:app/users/services/global_permissions.ts,app/common/ui/hooks/use_can.ts. - Central registries:
app/core/policies/main.ts,app/core/abilities/main.ts.
Doc refs
- AdonisJS Bouncer — https://docs.adonisjs.com/guides/security/authorization
Workflow
Add a new permission
- Add an entry to
PERMISSIONS:
``ts export const PERMISSIONS = { ..., invoicesCreate: 'invoices.create', } as const ``
- Reference it in the policy method that gates it:
``ts async create(user: User) { return user.hasPermission(PERMISSIONS.invoicesCreate) } ``
- Grant it to the roles that need it (usually via the seeder for
admin). - If the frontend needs a boolean for the permission, extend
GlobalPermissions:
``ts canCreateInvoices: user?.hasPermission(PERMISSIONS.invoicesCreate) ?? false, ``
Add a new role
- Add slug to
ROLES+ weight toROLE_WEIGHTS. - Seed the role with its permissions in a seeder.
- Update
ensureBaseRoles()in the test RBAC helpers if it's a base role every test needs.
Gate a route (mutation)
Policy runs before the validator so a 403 doesn't leak field-level errors to unauthorized callers. See [[crud]].
Gate a UI element
Or use the built-in can: 'permissionKey' field on nav items — see [[layout-shells]].
Escalation-safe user creation / invite
In the action that creates a user with a non-default role:
The validator alone isn't enough — a user with users.create but not users.manage_roles could try to escalate. This guard is defense-in-depth.
Testing
Set roles per test with the RBAC helpers:
Cover the "unauth", "authenticated-but-forbidden", and "authorized" paths for each mutation. See [[testing]].
Anti-patterns
- ❌ Hardcoding role name strings (
'admin') — useROLES.ADMIN. - ❌ Hardcoding permission strings (
'users.create') — usePERMISSIONS.usersCreate. - ❌ Skipping the policy call because "the validator already checked" — validators check payload shape, not authority.
- ❌ Adding a permission to
PERMISSIONSand forgetting the seeder — nobody will actually have it. - ❌ Gating only on the frontend (
if (can.x)) — always mirror the check on the backend policy. Frontend gating is UX, not security. - ❌ Assigning role by slug string in tests instead of
withRole(user, ROLES.ADMIN)— drifts if the slug ever changes. - ❌ Removing an escalation guard "because the validator handles it" — it's defense in depth for a reason.
- ❌ Owner check via
.where('owner_id', user.id)in a query instead of the policy — the policy is the source of truth; a query filter is an unrelated concern.
Related skills
[[crud]] · [[testing]] · [[actions-events]] · [[layout-shells]]
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingpackages/skills/authorization/SKILL.md
Popularity
- Repository stars
- 94
- Repository forks
- 12
- 7-day trend score
- 23
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/filipebraida/adonisjs-starter-kit/authorization)