Permissions & Role Guards
Permissions & Role Guards
nuxt-protokit includes a lightweight permission system that lets you guard collections and fields based on the current user's roles. The guards drive UI behaviour — hiding collections, disabling controls, and making fields read-only — without requiring you to build this logic into every component by hand.
How it works
The permission system has two parts:
- A role resolver — a function you provide that returns the current user's roles.
- Permission guards — declarative rules in your schema that reference those roles.
The two are connected via configureProtoPermissions(), which you call once in a Nuxt plugin.
Setup
Register your role resolver before any component that uses permission-aware composables is mounted. A client-side plugin is the right place for this.
export default defineNuxtPlugin(() => {
configureProtoPermissions({
resolveRoles: () => useAuthUser().value?.roles ?? [],
})
})
resolveRoles is called reactively every time a permission is evaluated, so it will always reflect the current authentication state.
Permission guards
A guard can be written in three ways:
// Single role — user must hold this exact role
permissions: { read: 'admin' }
// Role array — user must hold at least one of these roles
permissions: { read: ['admin', 'editor'] }
// Custom function — full control, receives a PermissionContext
permissions: {
read: (ctx) => ctx.roles.includes('admin') || someOtherCondition,
}
Omitting a guard means allow — no guard defined is always open access.
Collection permissions
Add a permissions key to any CollectionSchema. The four available actions are read, create, update, and delete.
defineCollection({
key: 'tasks',
title: 'Tasks',
// ...
permissions: {
read: ['admin', 'editor', 'viewer'],
create: ['admin', 'editor'],
update: ['admin', 'editor'],
delete: ['admin'],
},
})
Consuming collection permissions
useProtoCollection returns a reactive permissions object. Use it to show or hide controls in your template:
<script setup lang="ts">
const { items, add, remove, permissions } = useProtoCollection(tasksSchema)
</script>
<template>
<div v-if="permissions.canRead.value">
<ul>
<li v-for="(task, i) in items" :key="i">
{{ task.title }}
<button
v-if="permissions.canDelete.value"
@click="remove(i)"
>
Delete
</button>
</li>
</ul>
<button
v-if="permissions.canCreate.value"
@click="add({ title: '' })"
>
Add task
</button>
</div>
</template>
When using usePrototype, each collection's permissions are available on collectionPermissions:
const { collections, collectionPermissions } = usePrototype(schema)
// collectionPermissions.tasks.canCreate.value → boolean
// collectionPermissions.tasks.canDelete.value → boolean
Field permissions
Add a permissions key to any field definition. Two actions are available: read (visibility) and write (editability).
definePrototype({
key: 'budget-tool',
// ...
fields: {
name: {
type: 'text',
label: 'Name',
default: '',
},
internalBudget: {
type: 'number',
label: 'Internal Budget',
default: 0,
permissions: {
read: ['admin', 'finance'],
write: ['admin'],
},
},
},
})
Consuming field permissions
useProtoMap and usePrototype return a fieldPermissions object keyed by field name:
<script setup lang="ts">
const { state, fieldPermissions } = usePrototype(schema)
</script>
<template>
<input v-model="state.name.value" />
<input
v-if="fieldPermissions.internalBudget.canRead.value"
v-model="state.internalBudget.value"
:disabled="!fieldPermissions.internalBudget.canWrite.value"
/>
</template>
The ProtoForm component reads fieldPermissions automatically — fields with canRead: false are hidden, and fields with canWrite: false are rendered as read-only inputs.
Ad-hoc checks
useProtoPermissions() exposes the underlying can and canField helpers for one-off checks outside a schema context:
const { can, canField, getRoles } = useProtoPermissions()
// Check a collection action
const allowed = can('delete', myCollectionPermissions, { collection: 'tasks' })
// Check a field action
const visible = canField('read', myFieldPermissions, { field: 'budget' })
// Inspect current roles
const roles = getRoles() // string[]
Limitations
Understanding what this system does not provide is as important as knowing what it does.
Field-level permissions are cosmetic
Field guards hide or disable UI elements. The Y.js document itself is always fully loaded in the browser — every field value is present in memory and accessible via the browser console or devtools. A user who understands the Y.js CRDT API can read or write any field directly without going through your Vue components.
Treat field permissions as a UX convenience, not a security boundary. They are appropriate for:
- Hiding irrelevant fields from users who should not be concerned with them.
- Preventing accidental edits to fields a role should not normally change.
- Reducing visual clutter for limited-access roles.
They are not appropriate for:
- Hiding genuinely sensitive values (API keys, private identifiers, confidential figures).
- Preventing a motivated user from modifying data.
Collection-level permissions need server enforcement
Collection guards disable the UI controls for adding, editing, and deleting items. The Y.js document is still synced to the server and other peers, so a user who bypasses the UI can still push changes.
For collections that must be protected, implement access control in your server sync handler (e.g. validate the user's role in your /api/yjs route before applying updates). The read guard hides the collection in the UI, but the underlying data array is still in the document.
The role resolver runs in the browser
resolveRoles executes client-side. Any role information it returns came from the browser — it can be tampered with. Never use browser-derived role data as the sole gate on a destructive or privileged server operation.
No encryption
Permission guards do not encrypt document data. Restricting what the UI renders does not prevent a user from reading raw IndexedDB entries or inspecting network payloads.
Summary
| Scope | What the guard does | Enforcement required elsewhere |
|---|---|---|
| Collection read | Hides the collection UI | Server: validate before syncing |
| Collection write | Disables add / edit / delete controls | Server: validate before applying update |
| Field read | Hides the field from the form | Nothing reliable; avoid sensitive data |
| Field write | Renders the field as read-only | Nothing reliable; treat as UX only |
Custom Fields & Viz
Add custom field components and visualization types to nuxt-protokit without modifying the module source, using defineProtokitExtension and the protokit:register-extension hook.
Business Logic Guards
Extend role-based permission guards with reactive application state — project status, ownership, quotas, and other runtime conditions — using Vue's computed().