Permissions
The complete permission vocabulary, owner-only actions, and the per-endpoint gate matrix.
Authorization in Clog is per-workspace. A member can do something only if their Membership carries the matching permission — or if they're the workspace owner (in which case every permission is implicit).
This page is the full lookup. For the dashboard authoring walkthrough — inviting members, picking permissions — see Members and permissions.
Roles
| Role | Stored on | Meaning |
|---|---|---|
owner | Membership.role | Implicit holder of every permission below. Sole holder of owner-only actions. Exactly one per workspace. |
member | Membership.role | Holds only the permissions on its permissions: string[] grant list. |
The vocabulary
Ten read scopes and ten write scopes. The list is a closed enum — unknown values are rejected at the wire.
posts:read posts:write
pages:read pages:write
categories:read categories:write
tags:read tags:write
authors:read authors:write
media:read media:write
members:read members:write
api-keys:read api-keys:write
settings:read settings:write
seo:read seo:write| Scope | :read grants | :write grants |
|---|---|---|
| posts | View posts (including drafts). | Create, edit, publish, archive, soft-delete posts. Edit schemaType / structuredData (content). |
| pages | View pages. | Create, edit, publish, soft-delete pages. |
| categories | View the category tree. | Create, rename, reparent, delete categories. |
| tags | View tags. | Create, rename, delete tags. |
| authors | View author profiles. | Create, edit, delete author personas. |
| media | View the media library. | Upload, edit alt/caption, delete media files. |
| members | View the Members and Pending invites tabs. | Invite, edit permissions, remove members. |
| api-keys | View API keys (prefix only). | Create, edit, reveal, revoke API keys. |
| settings | View workspace settings (General, Branding, SEO defaults). | Edit them. |
| seo | View per-entity SEO meta and workspace SEO defaults. | Edit them — see the cross-cutting rule. |
seo:* is cross-cutting
The two SEO scopes interact with every content entity. The rule:
| Field surface | Required permission |
|---|---|
Per-entity SEO meta (title, description, canonicalUrl, focus keyword, robots, OG, Twitter, breadcrumbs title) on Post / Page / Category / Tag / Author | <entity>:write OR seo:write |
Post schemaType + structuredData (reader-facing structured content — recipes, how-tos, etc.) | posts:write only — it's content, not metadata, so seo:write doesn't unlock it |
| Workspace SEO defaults + publisher info (read) | settings:read OR seo:read |
| Workspace SEO defaults + publisher info (write) | settings:write AND seo:write |
| Redirects + Link-health (read) | settings:read OR seo:read |
| Redirects + Link-health (write) | settings:write OR seo:write |
Practical interpretation: seo:write is the "SEO specialist" scope. Hand it to someone who tunes titles, descriptions, robots, and social metadata across every entity without giving them content-edit rights. To let them also touch a recipe's ingredients or a how-to's steps, give them posts:write — but that crosses into content territory.
Owner-only actions
Three actions cannot be granted to a member at any permission level. They use the dedicated requireWorkspaceOwner gate, not requirePermission:
| Action | Why |
|---|---|
| Delete the workspace | Destructive cascade — removes every post, page, member, key, redirect. Owner only. |
| Transfer ownership | Re-assigns the single owner role to another existing member. Owner only. |
| Edit the owner's permissions | The owner's permission list is always implicitly full — direct edits to it are rejected, even by the owner themselves. To remove an owner's power, transfer ownership first. |
Permission flow per request
For dashboard requests (/api/v1/*):
authMiddleware → req.auth = { userId, sessionId }
requireWorkspaceMembership → req.auth.workspaceId, req.auth.membership
requirePermission("posts:write") → owners pass; members check permissions.includes(...)
requireWorkspaceOwner → role === "owner"
handlerFor external API requests (/api/v1/external/*):
apiKeyMiddleware → req.auth = { userId, apiKeyId, workspaceId }
requireWorkspaceMembership → loads the creator's current membership
requirePermission("posts:read") → applies to the creator's current permission set
handlerAPI keys carry no permissions of their own. At request time the middleware resolves the creator's current Membership on the bound workspace and uses that membership's permission set for authorization.
Two consequences worth re-stating:
- Permission changes apply live. Removing
posts:writefrom a member instantly removes write access from every key they created. - Removing a member instantly disables all of their keys. The
ApiKeyrows still exist but stop authorising. For machine integrations, issue keys from a dedicated service-user account whose membership is stable.
Per-endpoint matrix — external API (/api/v1/external/*)
The full endpoint list — what's available to integrators and which scope each route needs.
Posts
| Method | Path | Permission |
|---|---|---|
GET | /external/posts | posts:read |
GET | /external/posts/:idOrSlug | posts:read |
POST | /external/posts | posts:write |
PATCH | /external/posts/:idOrSlug | posts:write |
DELETE | /external/posts/:idOrSlug | posts:write |
POST | /external/posts/:idOrSlug/tags | posts:write |
DELETE | /external/posts/:idOrSlug/tags | posts:write |
Pages
| Method | Path | Permission |
|---|---|---|
GET | /external/pages | pages:read |
GET | /external/pages/:idOrSlug | pages:read |
POST | /external/pages | pages:write |
PATCH | /external/pages/:idOrSlug | pages:write |
DELETE | /external/pages/:idOrSlug | pages:write |
Categories, tags, authors
| Method | Path | Permission |
|---|---|---|
GET | /external/categories | categories:read |
GET | /external/categories/:idOrSlug | categories:read |
POST/PATCH/DELETE | /external/categories[/...] | categories:write |
GET | /external/tags | tags:read |
GET | /external/tags/:idOrSlug | tags:read |
POST/PATCH/DELETE | /external/tags[/...] | tags:write |
GET | /external/authors | authors:read |
GET | /external/authors/:idOrSlug | authors:read |
POST/PATCH/DELETE | /external/authors[/...] | authors:write |
Media
| Method | Path | Permission |
|---|---|---|
GET | /external/media | media:read |
GET | /external/media/:id | media:read |
POST | /external/media (multipart) | media:write |
PATCH | /external/media/:id | media:write |
DELETE | /external/media/:id | media:write |
Workspace + Redirects + Link health + Feeds
| Method | Path | Permission |
|---|---|---|
GET | /external/workspace | settings:read |
GET | /external/redirects | settings:read OR seo:read |
POST | /external/link-health | none beyond workspace membership — intentionally low-friction so consumer sites can fire-and-forget on every 404 |
GET | /external/feeds/sitemap-data | posts:read |
Full endpoint shapes live in API → Endpoint reference.
Common grant scenarios
| Role you want to create | Scopes to grant |
|---|---|
| Content writer who only writes posts and edits images | posts:read posts:write media:read media:write categories:read tags:read authors:read |
| Content writer + tag / category manager | The above plus tags:write categories:write |
| Content writer + their own SEO | The above plus seo:read seo:write |
| SEO specialist (tune meta everywhere; no content edits) | posts:read pages:read categories:read tags:read authors:read seo:read seo:write |
| Integrator (manages API keys, doesn't write content) | api-keys:read api-keys:write — plus the read scopes for whatever the integration is going to fetch |
| Site admin (everything except ownership) | Tick every box. Owner-only actions (delete workspace, transfer ownership, edit owner's permissions) remain forbidden. |
| Read-only viewer | Every :read scope, no :write scopes. |
Related
- Dashboard guide → Members and permissions — invite teammates and pick scopes.
- Dashboard guide → SEO — the SEO surface's gating rule in context.
- API → Authenticating requests — how API keys map to permissions live.