Content Lake (Datastore)

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

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 the type field 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-body permissions[].name fields and in any other endpoint that takes a permission's name as 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.

RolePublish content?Manage content?Manage project infrastructure?Manage members and roles?
AdministratorYesYesFull (datasets, tokens, CORS, webhooks, GraphQL, deploy)Full, including adding administrators
EditorYesYesNoRead-only
ViewerNo (read-only)NoNoRead-only
ContributorNo (drafts only)Drafts onlyNoRead-only
DeveloperYesYesFull (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, staging only) or a single document type (for example, article only).
  • 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 assignees array."

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.dataset on sanity.document.filter.mode permissions.
  • 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[]._ref is rejected.
  • Subqueries in filter GROQ.
  • Filters that use user:: functions (like user::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/check with 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

Organization administrator default permissions

Where to go next

Was this page helpful?