Roles and permissions
Concepts behind Sanity's roles and permissions system: resources, permissions, roles, and users, plus when to reach for custom roles.
The Sanity Roles system is a granular way of attaching specific capabilities to specific groups of users. It is designed to function in a structured and flexible way. The goal of the Roles system is to provide a set of strong default permissions groups with an API for creating, managing, and using custom roles built the way your organization works with content.
Use the Access API
The preferred method for interacting with roles is the Access API. The project-based Roles API is still available, but the documentation below focuses on the Access API.
Role concepts
The Roles system consists primarily of:
- Resources
- Permissions
- Roles
- Members (users)
Resources
A resource defines an element of a Sanity project or organization on which a user can have special grants.
Today, a resource is either an organization or a project. Some predefined permissions also target additional resources like Media Library, Canvas, Dashboard, and View; the API treats these as resource types in their own right.
Permissions
Every resource has a list of permissions. These permissions represent actions that can be performed on the resource. A user or robot must be granted a permission (through a role) in order to perform the action.
The permission typically takes the form of {company}.{resourceType}.{objectName}.{action}, but this is not always the case due to legacy terms.
There are both predefined and custom permissions. Predefined permissions are included with the product and are not editable.
Permission names: dotted vs. hyphenated
Permissions are identified two ways depending on the field or endpoint:
- The dotted form (
sanity.project.members,sanity.document.filter.mode) identifies the permission's type. Use it in thetypefield of a custom permission body and in/user-permissions/me/check?permissions=...queries (where you collapse type and action into a single dotted string, for example,sanity.project.members.read). - The hyphenated form (
sanity-project-members,sanity-document-filter-drafts) identifies a specific predefined permission resource by name. Use it in role-bodypermissions[].namefields and in any other endpoint that takes a permission'snameas a parameter, including path parameters like/permissions/{permissionName}.
Both forms refer to the same underlying permission. Role bodies use the hyphenated form because permissions[].name takes a permission's identifier name. Custom permissions you create use a name you choose; they aren't predefined and don't have a dotted-vs-hyphenated equivalence.
Roles
Roles define a set of grants that project members can have assigned to them. A project member can have many roles and grants, even within the same organization.
A role is a named bundle of permissions plus two flags that control who the role can be assigned to:
{
"name": "article-editor",
"title": "Article editor",
"appliesToUsers": true,
"appliesToRobots": false,
"permissions": [
{ "name": "sanity-project", "action": "read" },
{ "name": "articles-only", "action": "update" },
{ "name": "articles-only", "action": "create" }
]
}Each entry in permissions is a {name, action, params?} object. name references either a predefined permission or a custom permission you've created in the same resource. action is the action you're granting. params is optional and used for permission types that take action parameters, like mode and dataset scoping on sanity.document.filter.mode.
The appliesToUsers and appliesToRobots flags control which kinds of subjects can hold the role. A role intended for CI pipelines should set appliesToUsers: false and appliesToRobots: true so it can't be assigned to a person by mistake.
Default roles
By default, there are specifically defined roles available for each plan type. Custom roles are available for Enterprise customers.
- All plans
- Administrator: Read and write access to all datasets, with full access to all project settings.
- API Tokens
- Editor Token (read+write)
- Viewer Token (read-only)
- Free
- Administrator
- Viewer: Can view all documents in all datasets within the project.
- Growth
- Administrator
- Editor
- Viewer
- Developer
- Contributor: Read and write access to draft content within all datasets, with no access to project settings.
The table below summarizes what each built-in role grants. You can use these as-is, assign multiple to the same user, or use them as a baseline to compare custom roles against.
| Role | Publish content? | Manage content? | Manage project infrastructure? | Manage members and roles? |
|---|---|---|---|---|
| Administrator | Yes | Yes | Full (datasets, tokens, CORS, webhooks, GraphQL, deploy) | Full, including adding administrators |
| Editor | Yes | Yes | No | Read-only |
| Viewer | No (read-only) | No | No | Read-only |
| Contributor | No (drafts only) | Drafts only | No | Read-only |
| Developer | Yes | Yes | Full (datasets, tokens, CORS, webhooks, GraphQL, deploy) | Invite, read, and update (cannot delete members) |
The five built-in roles also carry implied organization-level roles for Media Library, Canvas, and Dashboard (where those apps are available on your plan). This is automatic, and worth knowing about: assigning a project Editor role also grants organization-level editor access to those apps. If you're designing a custom role to restrict a user to a single project surface, factor in the implied organization-level access from any roles they already hold.
Members (users)
A user is a person who has one or more roles assigned to them.
A user is initially added to a resource via invitation or access request. A user who already has one role can be assigned roles in another project within the same organization or at the organization level without requiring a separate invite.
As an organization owns multiple resources, such as projects, any users with roles on these resources are also returned when reading the users of an organization.
If a user has roles in multiple projects, they are considered a single user and can be referenced by their sanityUserId. For example, inviting user A to project B and project C in the same organization will result in a single user with two memberships.
Custom roles
When the built-in roles don't fit, you can author your own with the Access API. Custom roles are an Enterprise plan feature.
When to reach for a custom role
The built-in roles are the right answer for most projects. Reach for a custom role when you need one of these patterns:
- A CI/CD pipeline that deploys the Studio, but must not be able to mutate content or manage users.
- An editor scoped to a single dataset (for example,
stagingonly) or a single document type (for example,articleonly). - A release reviewer who can schedule releases but not publish them.
- A read-only role for a business intelligence tool that queries production data via the API.
- An attribute-based role where the permission depends on a runtime check, like "the user is listed in the document's
assigneesarray."
Each of these maps to a recipe in Build a custom role with the Access API.
What custom roles can express
Custom roles are powerful but not unlimited. Knowing the boundaries up front saves design time.
You can express:
- Document-type matching with GROQ filters (for example,
_type == "article"). - Attribute comparisons (
status == "published",priority > 3). - Identity-based filtering with
identity()(the calling user's Sanity user ID). - User-attribute templating with
user::attributes()for attribute-based access control. - Dataset scoping via
params.datasetonsanity.document.filter.modepermissions. - Mode scoping (
read,create,publish) for read-only roles, draft-only contributors, and full-publish editors.
You can't express:
- Joins or reference traversal in filter GROQ. A filter like
*[_type == "user" && _id == identity()].assignedDocs[]._refis rejected. - Subqueries in filter GROQ.
- Filters that use
user::functions (likeuser::attributes()) unless the user-attributes feature is enabled for your project; permission creation rejects them otherwise. - A "review-but-not-comment" role. Comments and mentions check read permission only; there's no separate write check for leaving a comment, so you can't grant read while denying comment.
If your role needs something filter GROQ can't express, the workaround is usually to denormalize the data (put the value you need to check directly into the document) or to gate the feature higher up in your application.
Creating a custom role
This is a paid feature
This feature is available on certain Enterprise plans. Talk to sales to learn more.
You create custom roles with the Access API: define any custom permissions, bundle them into a role, and assign the role to users or robots. For the four-step flow, six copy-paste recipes, and verification patterns, see Build a custom role with the Access API.
How custom roles take effect
When you create or modify a role via the Access API, Sanity propagates the change to the Content Lake, typically within seconds. After propagation, the new permissions apply to the user's or robot's next request.
Two things to know about propagation in practice:
- Studio sessions cache permissions: A running Studio app may show stale permission state until the user reloads. After you assign a new role, ask the user to refresh the browser to pick up the change.
- For automated checks: Call
GET /v2025-07-11/access/{resourceType}/{resourceId}/user-permissions/me/checkwith the user's session token to verify a role has applied. See the verification section in Build a custom role for the request shape and a CI-friendly testing pattern.
Administrator default permissions
The following are the permissions for the default "Administrator" role available for various plans. You can view your default permission resources with the Access API.
Project administrator default permissions
{
"name": "administrator",
"title": "Administrator",
"description": "Read and write access to all datasets, with full access to all project settings.",
"isCustom": false,
"resourceId": "3do82whm",
"resourceType": "project",
"appliesToUsers": true,
"appliesToRobots": false,
"permissions": [
{
"name": "sanity-project",
"action": "read",
"params": {}
},
{
"name": "sanity-project",
"action": "update",
"params": {}
},
{
"name": "sanity-project",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-members",
"action": "invite",
"params": {}
},
{
"name": "sanity-project-members",
"action": "update",
"params": {}
},
{
"name": "sanity-project-members",
"action": "read",
"params": {}
},
{
"name": "sanity-project-roles",
"action": "create",
"params": {}
},
{
"name": "sanity-project-roles",
"action": "read",
"params": {}
},
{
"name": "sanity-project-roles",
"action": "update",
"params": {}
},
{
"name": "sanity-project",
"action": "deployStudio",
"params": {}
},
{
"name": "sanity-project",
"action": "createSession",
"params": {}
},
{
"name": "sanity-project-members",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-roles",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-datasets",
"action": "create",
"params": {}
},
{
"name": "sanity-project-datasets",
"action": "read",
"params": {}
},
{
"name": "sanity-project-datasets",
"action": "update",
"params": {}
},
{
"name": "sanity-project-tags",
"action": "create",
"params": {}
},
{
"name": "sanity-project-tags",
"action": "read",
"params": {}
},
{
"name": "sanity-project-tags",
"action": "update",
"params": {}
},
{
"name": "sanity-project-tokens",
"action": "create",
"params": {}
},
{
"name": "sanity-project-tokens",
"action": "read",
"params": {}
},
{
"name": "sanity-project-tokens",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-datasets",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-tags",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-cors",
"action": "create",
"params": {}
},
{
"name": "sanity-project-cors",
"action": "read",
"params": {}
},
{
"name": "sanity-project-cors",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-webhooks",
"action": "create",
"params": {}
},
{
"name": "sanity-project-webhooks",
"action": "read",
"params": {}
},
{
"name": "sanity-project-webhooks",
"action": "update",
"params": {}
},
{
"name": "sanity-project-graphql",
"action": "manage",
"params": {}
},
{
"name": "sanity-project-usage",
"action": "read",
"params": {}
},
{
"name": "sanity-project-webhooks",
"action": "delete",
"params": {}
},
{
"name": "sanity-all-documents",
"action": "mode",
"params": {
"mode": "publish",
"history": true
}
}
]
}https://api.sanity.io/v2025-07-11/access/project/{projectId}/roles/administratorOrganization administrator default permissions
{
"name": "administrator",
"title": "Administrator",
"description": "Administrators can manage billing details, legal contacts, organization members and manage project ownership",
"isCustom": false,
"resourceId": "oSyH1iET5",
"resourceType": "organization",
"appliesToUsers": true,
"appliesToRobots": false,
"permissions": [
{
"name": "sanity-organization",
"action": "read",
"params": {}
},
{
"name": "sanity-organization",
"action": "update",
"params": {}
},
{
"name": "sanity-organization",
"action": "delete",
"params": {}
},
{
"name": "sanity-organization",
"action": "billing",
"params": {}
},
{
"name": "sanity-organization-projects",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-projects",
"action": "attach",
"params": {}
},
{
"name": "sanity-organization-projects",
"action": "detach",
"params": {}
},
{
"name": "sanity-organization-legal",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-legal",
"action": "update",
"params": {}
},
{
"name": "sanity-organization-members",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-members",
"action": "delete",
"params": {}
},
{
"name": "sanity-organization-members",
"action": "update",
"params": {}
},
{
"name": "sanity-organization-members",
"action": "invite",
"params": {}
},
{
"name": "sanity-organization-roles",
"action": "create",
"params": {}
},
{
"name": "sanity-organization-roles",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-roles",
"action": "update",
"params": {}
},
{
"name": "sanity-organization-roles",
"action": "delete",
"params": {}
},
{
"name": "sanity-organization-tokens",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-tokens",
"action": "create",
"params": {}
},
{
"name": "sanity-organization-tokens",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-members",
"action": "read",
"params": {}
},
{
"name": "sanity-project-members",
"action": "delete",
"params": {}
},
{
"name": "sanity-project-members",
"action": "update",
"params": {}
},
{
"name": "sanity-project-members",
"action": "invite",
"params": {}
},
{
"name": "sanity-media-library",
"action": "read",
"params": {}
},
{
"name": "sanity-media-library-members",
"action": "read",
"params": {}
},
{
"name": "sanity-media-library-members",
"action": "delete",
"params": {}
},
{
"name": "sanity-media-library-members",
"action": "update",
"params": {}
},
{
"name": "sanity-media-library-members",
"action": "invite",
"params": {}
},
{
"name": "sanity-sdk-applications",
"action": "read",
"params": {}
},
{
"name": "sanity-sdk-applications",
"action": "deploy",
"params": {}
},
{
"name": "sanity-sdk-applications",
"action": "delete",
"params": {}
},
{
"name": "sanity-project",
"action": "read",
"params": {}
},
{
"name": "sanity-project",
"action": "deployStudio",
"params": {}
},
{
"name": "sanity-dashboard-configuration-organization",
"action": "read",
"params": {}
},
{
"name": "sanity-dashboard-configuration-organization",
"action": "update",
"params": {}
},
{
"name": "sanity-dashboard-configuration-organization",
"action": "create",
"params": {}
},
{
"name": "sanity-view",
"action": "read",
"params": {}
},
{
"name": "sanity-view",
"action": "update",
"params": {}
},
{
"name": "sanity-view",
"action": "create",
"params": {}
},
{
"name": "sanity-view",
"action": "delete",
"params": {}
},
{
"name": "sanity-organization-views",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-views",
"action": "update",
"params": {}
},
{
"name": "sanity-organization-views",
"action": "create",
"params": {}
},
{
"name": "sanity-organization-views",
"action": "delete",
"params": {}
},
{
"name": "sanity-dashboard-intents",
"action": "create",
"params": {}
},
{
"name": "sanity-dashboard-intents",
"action": "update",
"params": {}
},
{
"name": "sanity-dashboard-intents",
"action": "delete",
"params": {}
},
{
"name": "sanity-view",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-sessions",
"action": "read",
"params": {}
},
{
"name": "sanity-organization-sessions",
"action": "delete",
"params": {}
}
]
}https://api.sanity.io/v2025-07-11/access/organization/{organizationId}/roles/administratorWhere to go next
- Build a custom role with the Access API: the four-step flow, six recipes covering the most common scenarios, and a reference table mapping intents to permissions.
- Access API HTTP reference: the full endpoint inventory.