About using-backstage-permissions
A Backstage development skill for implementing permission checks in plugins. It explains shared permission declarations and frontend or backend authorization, including resource-specific decisions.
Key features
- 01Declare shared plugin permissions
- 02Gate frontend UI with permission checks
- 03Authorize backend actions
- 04Make conditional resource-level decisions
- 05Handle asynchronous permission states
Use cases
- 01Hiding controls from unauthorized users
- 02Checking permission before a route action
- 03Restricting updates based on resource ownership
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ vinzscam/ backstage-skills --skill using-backstage-permissionsAfter installing, type/using-backstage-permissionsin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/backend.mddeclaring.mdfrontend.mdresource-permissions.mdDetails
From the project READMEBackstage Permissions Patterns
When to use this skill
- Plugin needs to hide a button, menu, or page from users without the right permission (frontend)
- Plugin's route handler needs to authorize the caller before performing an action (backend)
- Defining new permissions for a plugin
- Checking permissions per-resource ("can update this component", not "can update components in general")
Choose the right pattern
Declaring permissions
| Building... | Pattern | Reference |
|---|---|---|
| A new permission for your plugin | createPermission({ name, attributes }) in a *-common library | references/declaring.md |
Frontend (in a plugin component)
| Situation | Pattern | Reference |
|---|---|---|
| Conditionally render UI based on a permission | usePermission({ permission }) hook | references/frontend.md |
| Wrap an entire subtree in a permission gate | <RequirePermission permission={...}>...</RequirePermission> | references/frontend.md |
Backend (in a route handler or scheduled task)
| Situation | Pattern | Reference |
|---|---|---|
| Authorize a basic action permission | coreServices.permissions.authorize([{ permission }], { credentials }) | references/backend.md |
| Authorize per-resource (with conditional decisions) | permissions.authorizeConditional([{ permission, resourceRef }], { credentials }) + permissionsRegistry.addResourceType(...) | references/resource-permissions.md |
Two invocation modes
- Standalone (mid-development): user is editing an existing plugin. Pick the right reference based on what's being added — defining a permission, gating UI, or authorizing on the backend.
- Post-scaffold (called by
creating-backstage-plugin): when intent describes "only admins can…", "the current user must have permission to…", or any access-control language. Add the relevant patterns to the freshly scaffolded plugin.
Common gotchas
- Permissions are declared once, used twice. The permission object lives in a
*-common(or*-permissions-common) library so both the frontend (usePermission) and the backend (permissions.authorize) import the same definition. Don't redeclare the same permission in two places. - The host app must enable the permission framework. Adopters set
permission.enabled: trueinapp-config.yamland install aPermissionPolicy. If permissions aren't enabled,authorizealways returnsAuthorizeResult.ALLOW— your plugin code is correct, just unenforced. Document the prerequisite in your plugin README. permissions.authorizereturns an array matching the order of requests. For a single permission, destructureconst [decision] = await permissions.authorize(...)— and the decision isdecision.result === AuthorizeResult.ALLOW, NOT a boolean.- For resource permissions, prefer
authorizeConditionaloverauthorizewhen the decision can depend on the resource (e.g. "owner can update"). It returns conditional decisions you translate into a database-level filter, avoiding the need to load every candidate resource into memory. usePermissionis async. Returns{ allowed, loading, error }— render a<Skeleton />while loading, otherwise users see UI flicker as access is determined.- For backend plugins,
coreServices.permissionsandcoreServices.permissionsRegistrycome from@backstage/backend-plugin-api. They're plugin-scoped — your plugin gets aPermissionsServicethat already knows its own pluginId.
Comments
Markdown supported