Admin Permissions

SkillDev tools

Helps add role-based permission screens and checks to Webiny admin interfaces.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Admin Permissions skill

About this skill

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 HasP

What this skill tells your AI

The instructions your AI receives, as published by webiny/webiny-js in skills/user-skills/admin/admin-permissions/SKILL.md and read by ahel’s review.

Overview

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.

Layer 1: Domain — Permission Schema

Define the schema in src/domain/permissionsSchema.ts:

import { 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"]
    }
  ]
});

Schema Reference

FieldTypeRequiredDescription
prefixstringYesPermission prefix (e.g., "sm")
fullAccesstrue | objectYestrue for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., { canForceUnlock: true }).
readOnlyAccessbooleanNoWhether to show a "Read-only access" option
entitiesEntityDefinition[]NoEntity definitions. Omit for binary full/no access.

Entity Definition

FieldTypeRequiredDescription
idstringYesUnique identifier for form field naming
titlestringNoDisplay title. Falls back to id.
permissionstringYesPermission name emitted (e.g., "sm.product")
scopes("full" | "own")[]YesAvailable access scopes
actionsActionDefinition[]NoActions on this entity
dependsOn{ entity: string; requires: string }NoDependency on another entity

Actions

  • { 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.

Entity Dependencies

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.

{
    id: "review",
    permission: "sm.review",
    scopes: ["full", "own"],
    actions: [{ name: "rwd" }],
    dependsOn: { entity: "product", requires: "r" }
}

Simple Apps (No Entities)

Omit entities for binary full/no access:

export const MA_PERMISSIONS_SCHEMA = createPermissionSchema({
  prefix: "ma",
  fullAccess: true
});

Layer 2: Features — DI Artifacts + Registration

Abstraction (src/features/permissions/abstractions.ts)

import { 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>;
}

Feature (src/features/permissions/feature.ts)

import { 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);

Extension Registration

Register the feature and the permission UI in your extension component:

import { 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>
    </>
  );
};

Layer 3: Presentation — Hooks & Components

usePermissions Hook

// src/presentation/security/usePermissions.ts
import { createUsePermissions } from "webiny/admin/security";
import { SmPermissions } from "~/features/permissions/abstractions.js";

export const usePermissions = createUsePermissions(SmPermissions);

Usage:

const 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 Component

// 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:

<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>

DI Injection in Features

Inject permissions into feature implementations:

import 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

PropTypeRequiredDescription
namestringYesUnique identifier for this permission renderer
titlestringYesDisplay title in the accordion header
descriptionstringNoDescription shown below the title
iconReactElementNoIcon in the accordion header
schemaPermissionSchemaOne of schema/elementAuto-generate UI from schema
elementReactElementOne of schema/elementFully custom permission UI
systembooleanNoIf true, renders before app-level permissions

File Structure

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.Permissions

Matching API-Side Permissions

The 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.

Related Skills

  • webiny-api-permissions — API-side permission checking (canRead, canEdit, etc.)
  • webiny-admin-architect — Admin architecture, headless and presentation features
  • webiny-admin-ui-extensions — Admin UI customization, decorators, config

Signals

GitHub stars
8k
Forks
679
Last commit
Sep 2026
Advanced
Item type
skill
Key
webiny-admin-permissions
Source
github.com/webiny/webiny-js