Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Admin-side permission UI registration and DI-backed permission checking. Use this skill when adding permission controls to the admin UI — schema-based auto-generated forms, injectable permissions via createPermissionsAbstraction/ createPermissionsFeature, typed hooks (createUsePermissions), the HasPermission component (createHasPermission), and the Security.Permissions component props. Covers both simple apps and complex multi-entity permission schemas.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 34% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 55% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 25% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 91% | 0% |
Permissions follow three layers: domain (schema), features (DI artifacts + registration), and presentation (hooks, components, UI config). The framework auto-generates the permission UI from a schema and provides injectable permission checking via the DI container.
Define the schema in src/domain/permissionsSchema.ts:
tsimport { createPermissionSchema } from "webiny/admin/security"; export const SM_PERMISSIONS_SCHEMA = createPermissionSchema({ prefix: "sm", fullAccess: true, entities: [ { id: "product", title: "Products", permission: "sm.product", scopes: ["full", "own"], actions: [ { name: "rwd" }, { name: "pw" }, { name: "import", label: "Import products" }, { name: "export", label: "Export products" } ] }, { id: "category", title: "Categories", permission: "sm.category", scopes: ["full"], actions: [{ name: "rwd" }] }, { id: "settings", title: "Settings", permission: "sm.settings", scopes: ["full"] } ] });
| Field | Type | Required | Description | | ---------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | prefix | string | Yes | Permission prefix (e.g., "sm") | | fullAccess | true \| object | Yes | true for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., { canForceUnlock: true }). | | readOnlyAccess | boolean | No | Whether to show a "Read-only access" option | | entities | EntityDefinition[] | No | Entity definitions. Omit for binary full/no access. |
| Field | Type | Required | Description | | ------------ | -------------------------------------- | -------- | ---------------------------------------------- | | id | string | Yes | Unique identifier for form field naming | | title | string | No | Display title. Falls back to id. | | permission | string | Yes | Permission name emitted (e.g., "sm.product") | | scopes | ("full" \| "own")[] | Yes | Available access scopes | | actions | ActionDefinition[] | No | Actions on this entity | | dependsOn | { entity: string; requires: string } | No | Dependency on another entity |
{ name: "rwd" } — Read/Write/Delete select dropdown. Auto-set to "rwd" when scope is "own".{ name: "pw" } — Publish/Unpublish checkbox group.{ name: "custom", label: "Label" } — Custom boolean flag.Child entities can depend on a parent. If the parent lacks the required action, the child is pruned from output. "own" scope cascades to dependents.
ts{ id: "review", permission: "sm.review", scopes: ["full", "own"], actions: [{ name: "rwd" }], dependsOn: { entity: "product", requires: "r" } }
Omit entities for binary full/no access:
tsexport const MA_PERMISSIONS_SCHEMA = createPermissionSchema({ prefix: "ma", fullAccess: true });
src/features/permissions/abstractions.ts)tsimport { createPermissionsAbstraction } from "webiny/admin/security"; import type { Permissions } from "webiny/admin/security"; import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js"; export const SmPermissions = createPermissionsAbstraction(SM_PERMISSIONS_SCHEMA); export namespace SmPermissions { export type Interface = Permissions<typeof SM_PERMISSIONS_SCHEMA>; }
src/features/permissions/feature.ts)tsimport { createPermissionsFeature } from "webiny/admin/security"; import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js"; import { SmPermissions } from "./abstractions.js"; export const SmPermissionsFeature = createPermissionsFeature(SM_PERMISSIONS_SCHEMA, SmPermissions);
Register the feature and the permission UI in your extension component:
tsximport { AdminConfig, RegisterFeature } from "webiny/admin/security"; import { ReactComponent as Icon } from "@webiny/icons/shield.svg"; import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js"; import { SmPermissionsFeature } from "~/features/permissions/feature.js"; const { Security } = AdminConfig; export const Extension = () => { return ( <> <RegisterFeature feature={SmPermissionsFeature} /> <AdminConfig> <Security.Permissions name="store-manager" title="Store Manager" description="Manage Store Manager permissions." icon={<Icon />} schema={SM_PERMISSIONS_SCHEMA} /> {/* Routes, menus, etc. */} </AdminConfig> </> ); };
usePermissions Hookts// src/presentation/security/usePermissions.ts import { createUsePermissions } from "webiny/admin/security"; import { SmPermissions } from "~/features/permissions/abstractions.js"; export const usePermissions = createUsePermissions(SmPermissions);
Usage:
tsconst permissions = usePermissions(); permissions.canAccess("product"); // has any access permissions.canRead("product"); // rwd includes "r" permissions.canCreate("product"); // rwd includes "w" permissions.canEdit("product", item); // rwd includes "w", respects own scope permissions.canDelete("product"); // rwd includes "d" permissions.canPublish("product"); // pw includes "p" permissions.canUnpublish("product"); // pw includes "u" permissions.canAction("import", "product"); // custom boolean flag
Entity IDs are fully typed — canRead("bogus") produces a type error.
HasPermission Componenttsx// src/presentation/security/HasPermission.tsx import { createHasPermission } from "webiny/admin/security"; import { SmPermissions } from "~/features/permissions/abstractions.js"; import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js"; export const HasPermission = createHasPermission(SmPermissions, SM_PERMISSIONS_SCHEMA);
Usage in JSX:
tsx<HasPermission entity="product"> {/* Rendered if user can access "product" */} </HasPermission> <HasPermission any={["product", "category"]}> {/* Rendered if user can access ANY of these entities */} </HasPermission> <HasPermission all={["product", "category"]}> {/* Rendered if user can access ALL of these entities */} </HasPermission> <HasPermission entity="product" action="read"> {/* Rendered if user canRead("product") */} </HasPermission> <HasPermission entity="product" allActions={["read", "publish"]}> {/* Rendered if user canRead AND canPublish "product" */} </HasPermission> <HasPermission entity="product" someActions={["import", "export"]}> {/* Rendered if user can do ANY of these custom actions */} </HasPermission>
Inject permissions into feature implementations:
tsimport type { SmPermissions } from "~/features/permissions/abstractions.js"; import { SmPermissions as SmPermissionsAbstraction } from "~/features/permissions/abstractions.js"; class SomeFeatureImpl { constructor(private permissions: SmPermissions.Interface) {} doSomething() { if (this.permissions.canEdit("product")) { // ... } } } const SomeFeature = SomeAbstraction.createImplementation({ implementation: SomeFeatureImpl, dependencies: [SmPermissionsAbstraction] });
Security.Permissions Props| Prop | Type | Required | Description | | ------------- | ------------------ | ------------------------- | ----------------------------------------------- | | name | string | Yes | Unique identifier for this permission renderer | | title | string | Yes | Display title in the accordion header | | description | string | No | Description shown below the title | | icon | ReactElement | No | Icon in the accordion header | | schema | PermissionSchema | One of schema/element | Auto-generate UI from schema | | element | ReactElement | One of schema/element | Fully custom permission UI | | system | boolean | No | If true, renders before app-level permissions |
src/
├── domain/
│ └── permissionsSchema.ts # createPermissionSchema()
├── features/
│ └── permissions/
│ ├── abstractions.ts # createPermissionsAbstraction() + namespace type
│ └── feature.ts # createPermissionsFeature()
├── presentation/
│ └── security/
│ ├── usePermissions.ts # createUsePermissions()
│ └── HasPermission.tsx # createHasPermission()
└── Extension.tsx # RegisterFeature + Security.PermissionsThe admin schema and the API-side createPermissions schema should use the same prefix, entity IDs, and action names. This ensures the permissions emitted by the admin UI are correctly evaluated by the API.
Admin: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
API: createPermissions({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })See webiny-api-permissions for the API-side implementation.
Other measured skills in the registry, with their headline benchmark lift.