Permissions
A layer declares capabilities. One backend layer registers a permissions driver that loads the capability list of the current user. Core reads that list to decide what the admin shows.
Declare a capability
export default defineNuxtPlugin(() => {
definePlutoExtension({
id: 'supabase-blog',
capabilities: [
{
id: 'posts-publish',
key: 'posts:publish',
label: 'Publish posts',
description: 'Change a post from draft to published.'
}
]
})
})
A capability key has the format <namespace>:<action>, lowercase. Examples: posts:publish, settings:manage.
blog.posts_per_page. Do not mix the two formats.key and id
A capability has both an id and a key. They hold different strings on purpose.
ididentifies the registry entry. The registry rewrites it to<extensionId>:<id>.keyis the capability string. The registry never changes it.
Use a plain id such as posts-publish. Put the real string in key.
usePlutoCapabilities()
const { items, groups, groupsFor } = usePlutoCapabilities()
items lists every declared capability. The list is for documentation and typing. The database stores capability strings as free text. A capability works even if no layer declares it.
groups groups the capabilities by owner, for a picker such as the Roles editor. A layer names its group with the locale key <owner>.capabilities.group. groupsFor(keys) adds an "Other" group for strings that no layer declares.
usePlutoPermissions()
const { granted, loaded, status, hasDriver, isAdmin, load, ensureLoaded, clear, can } = usePlutoPermissions()
await ensureLoaded(user.id)
can('posts:publish')
| Member | Meaning |
|---|---|
granted | The capability strings that the driver returned |
loaded | true after load() completed once |
status | 'idle' or 'loading' |
hasDriver | true when a layer registered a permissions driver |
isAdmin | true when granted includes * |
load(userId?) | Calls the driver. One request runs at a time. |
ensureLoaded(userId) | Returns at once when the list is loaded for this user. It rejects when the driver rejects. |
clear() | Resets the state. Core calls it on sign-out. |
can(capability) | true if the user holds the capability or * |
can() fails open when no permissions driver is registered. A site with no permissions backend has no server-side guard, so hiding UI would give no protection. Check hasDriver if you need to tell the two cases apart.can() is a UI convenience. It is never a security boundary. A user can call your API directly. The real enforcement is the server-side guard of the backend layer, backed by row-level security.Staff-only admin
Core uses the capability list in two places:
- The plugin
pluto-auth-sync.client.tsloads the list on sign-in and clears it on sign-out. It also syncs the admin language. - The route guard waits for the list. A signed-in user with no capability goes to
/admin/no-access.
See Admin shell.
Limits
- Core has no roles UI. The supabase layer adds one. See Users and roles.
- There are no capability hierarchies and no namespace wildcards. The only wildcard is
*. - A permissions driver for an admin must return
['*']. If it returns[], the admin is locked out of the admin pages.