Permissions & publish gates
Two related features: roles limit what a user can do inside the editor; publish gates validate the document before it goes live.
Roles
Pass role to <Blok>:
<Blok config={config} role={currentUser.role} />role is one of "editor" (default), "reviewer", or "viewer".
Role matrix
| Capability | editor | reviewer | viewer |
|---|---|---|---|
| View content | ✅ | ✅ | ✅ |
| Edit fields | ✅ | ||
| Add / remove blocks | ✅ | ||
| Comment | ✅ | ✅ | |
| Save drafts | ✅ | ||
| Publish | ✅ | ||
| Versions — save / restore | ✅ |
Import ROLE_MATRIX from @useblok/core if you want to read or check
the matrix programmatically:
import { ROLE_MATRIX, useCan, useRole } from "@useblok/core";
function Toolbar() {
const canPublish = useCan("publish");
const role = useRole();
return <div>You are {role}. {canPublish ? "✅" : "👀"}</div>;
}<IfCan> helper
Gate a UI branch on a permission:
import { IfCan } from "@useblok/core";
<IfCan action="publish">
<button>Publish now</button>
</IfCan>UI-side permissions are a UX guardrail, not a security boundary. Always
re-check permissions in your save / publish API handlers — never trust
the role prop alone.
Publish gates
A publish gate is a check that runs before onPublish fires. If any
gate returns an error-level issue, publishing is blocked and the user
sees a list of problems.
Pass a publishGates config to <Blok>:
<Blok
config={config}
publishGates={{
// Built-in: SEO readiness always runs.
// Set `comments: true` to also require all threads resolved.
comments: true,
// Custom gate — runs on every publish attempt.
custom: async (ctx) => {
const issues = [];
if (!ctx.data.root.props?.title) {
issues.push({
id: "missing-title",
severity: "error",
message: "Page title is required before publishing.",
});
}
return issues;
},
}}
onPublish={handlePublish}
/>Issue shape
interface GateIssue {
id: string;
severity: "error" | "warning";
message: string;
/** Optional: block ID to highlight + jump to. */
blockId?: string;
/** Optional: field name inside that block. */
fieldName?: string;
/** Optional: one-click fix. */
fix?: () => void;
}Gate context
Your custom gate receives:
interface GateContext {
data: Data;
config: Config;
}Built-in gates
| Gate | Enabled by | What it checks |
|---|---|---|
| SEO | always on | Meta title & description, heading hierarchy, image alt text. |
| Comments | publishGates.comments = true | Every comment thread is resolved. |
| Custom | your function | Anything you return from publishGates.custom. |
Reading the report elsewhere
If you want to show readiness somewhere else in the UI (dashboard,
header, etc.), use usePublishGates:
import { usePublishGates } from "@useblok/core";
function ReadinessBadge() {
const { issues, hasErrors } = usePublishGates();
return hasErrors ? (
<span>⚠️ {issues.length} issues</span>
) : (
<span>✅ Ready to publish</span>
);
}Server-side enforcement
Repeat after me: the editor’s role gates are UI only. Every
server-side write path (save, publish, schedule, unpublish, versions,
comments) must check permissions on its own. Treat Blok’s role prop as
hint-level — use it to hide buttons, not to protect data.