@wpkernel/core / defineCapability
Function: defineCapability()
function defineCapability<K>(config): CapabilityHelpers<K>;Define a capability runtime with declarative capability rules.
Capabilities provide type-safe, cacheable capability checks for both UI and actions. They enable conditional rendering (show/hide buttons), form validation (disable fields), and enforcement (throw before writes) - all from a single source of truth.
This is the foundation of Capability-Driven UI: Components query capabilities without knowing implementation details. Rules can leverage WordPress native capabilities (wp.data.select('core').canUser), REST probes, or custom logic.
What Capabilities Do
Every capability runtime provides:
can(key, params?)- Check capability (returns boolean, never throws)assert(key, params?)- Enforce capability (throwsCapabilityDeniedif false)- Cache management - Automatic result caching with TTL and cross-tab sync
- Event emission - Broadcast denied events via
@wordpress/hooksand BroadcastChannel - React integration -
useCapability()hook (provided by@wpkernel/ui) for SSR-safe conditional rendering - Action integration -
ctx.capability.assert()in actions for write protection
Basic Usage
import { defineCapability } from '@wpkernel/core/capability';
// Define capability rules
const capability = defineCapability<{
'posts.view': void; // No params needed
'posts.edit': number; // Requires post ID
'posts.delete': number; // Requires post ID
}>({
'posts.view': (ctx) => {
// Sync rule: immediate boolean
return ctx.adapters.wp?.canUser('read', { kind: 'postType', name: 'post' }) ?? false;
},
'posts.edit': async (ctx, postId) => {
// Async rule: checks specific post capability
const result = await ctx.adapters.wp?.canUser('update', {
kind: 'postType',
name: 'post',
id: postId
});
return result ?? false;
},
'posts.delete': async (ctx, postId) => {
const result = await ctx.adapters.wp?.canUser('delete', {
kind: 'postType',
name: 'post',
id: postId
});
return result ?? false;
}
});
// Use in actions (enforcement)
export const DeletePost = defineAction('Post.Delete', async (ctx, { id }) => {
ctx.capability.assert('posts.delete', id); // Throws if denied
await post.remove!(id);
ctx.emit(post.events.deleted, { id });
});
// Use in UI (conditional rendering)
function PostActions({ postId }: { postId: number }) {
const capability = useCapability<typeof capability>();
const canEdit = capability.can('posts.edit', postId);
const canDelete = capability.can('posts.delete', postId);
return (
<div>
<Button disabled={!canEdit}>Edit</Button>
<Button disabled={!canDelete}>Delete</Button>
</div>
);
}Caching & Performance
Results are automatically cached with:
- Memory cache - Instant lookups for repeated checks
- Cross-tab sync - BroadcastChannel keeps all tabs in sync
- Session storage - Optional persistence (set
cache.storage: 'session') - TTL support - Cache expires after configurable timeout (default: 60s)
const capability = defineCapability(rules, {
cache: {
ttlMs: 30_000, // 30 second cache
storage: 'session', // Persist in sessionStorage
crossTab: true // Sync across browser tabs
}
});Cache is invalidated automatically when rules change via capability.extend(), or manually via capability.cache.invalidate().
WordPress Integration
By default, capabilities auto-detect and use wp.data.select('core').canUser() for native WordPress capability checks:
// Automatically uses wp.data when available
const capability = defineCapability({
'posts.edit': async (ctx, postId) => {
// ctx.adapters.wp is auto-injected
const result = await ctx.adapters.wp?.canUser('update', {
kind: 'postType',
name: 'post',
id: postId
});
return result ?? false;
}
});Override adapters for custom capability systems:
const capability = defineCapability(rules, {
adapters: {
wp: {
canUser: async (action, resource) => {
// Custom implementation (e.g., check external API)
return fetch(`/api/capabilities?action=${action}`).then(r => r.json());
}
},
restProbe: async (key) => {
// Optional: probe REST endpoints for availability
return fetch(`/wp-json/acme/v1/probe/${key}`).then(r => r.ok);
}
}
});Event Emission
When capabilities are denied, events are emitted to:
@wordpress/hooks-{namespace}.capability.deniedwith full context- BroadcastChannel - Cross-tab notification for UI synchronization
- PHP bridge - Optional server-side logging (when
bridged: truein actions)
// Listen for denied events
wp.hooks.addAction('acme.capability.denied', 'acme-plugin', (event) => {
const reporter = createReporter({ namespace: 'acme.capability', channel: 'all' });
reporter.warn('Capability denied:', event.capabilityKey, event.context);
// Show toast notification, track in analytics, etc.
});Runtime Wiring
Capabilities are automatically registered with the action runtime on definition:
// 1. Define capability (auto-registers)
const capability = defineCapability(rules);
// 2. Use in actions immediately
const CreatePost = defineAction('Post.Create', async (ctx, args) => {
ctx.capability.assert('posts.create'); // Works automatically
// ...
});For custom runtime configuration:
globalThis.__WP_KERNEL_ACTION_RUNTIME__ = {
capability: defineCapability(rules),
jobs: defineJobQueue(),
bridge: createPHPBridge(),
reporter: createReporter()
};Extending Capabilities
Add or override rules at runtime:
capability.extend({
'posts.publish': async (ctx, postId) => {
// New rule
return ctx.adapters.wp?.canUser('publish_posts') ?? false;
},
'posts.edit': (ctx, postId) => {
// Override existing rule
return false; // Disable editing
}
});
// Cache automatically invalidated for affected keysType Safety
Capability keys and parameters are fully typed:
type MyCapabilities = {
'posts.view': void; // No params
'posts.edit': number; // Requires number
'posts.assign': { userId: number; postId: number }; // Requires object
};
const capability = defineCapability<MyCapabilities>({ ... });
capability.can('posts.view'); // ✅ OK
capability.can('posts.edit', 123); // ✅ OK
capability.can('posts.edit'); // ❌ Type error: missing param
capability.can('posts.unknown'); // ❌ Type error: unknown keyAsync vs Sync Rules
Rules can be synchronous (return boolean) or asynchronous (return Promise<boolean>). Async rules are automatically detected and cached to avoid redundant API calls:
defineCapability({
map: {
'fast.check': (ctx) => true, // Sync: immediate
'slow.check': async (ctx) => { // Async: cached
const result = await fetch('/api/check');
return result.ok;
}
}
});In React components, async rules return false during evaluation and update when resolved.
Type Parameters
K
K extends Record<string, unknown>
Capability map type defining capability keys and their parameter types
Parameters
config
Configuration object mapping capability keys to rule functions and runtime options
Returns
Capability helpers object with can(), assert(), keys(), extend(), and cache API
Throws
DeveloperError if a rule returns non-boolean value
Throws
CapabilityDenied when assert() called on denied capability
Examples
// Minimal example (no params)
const capability = defineCapability({
map: {
'admin.access': (ctx) =>
ctx.adapters.wp?.canUser('manage_options') ?? false
}
});
if (capability.can('admin.access')) {
// Show admin menu
}// With custom adapters
const capability = defineCapability({
map: rules,
options: {
namespace: 'acme-plugin',
adapters: {
restProbe: async (key) => {
const res = await fetch(`/wp-json/acme/v1/capabilities/${key}`);
return res.ok;
}
},
cache: { ttlMs: 5000, storage: 'session' },
debug: true // Log all capability checks
}
});