
Managing User Roles and Permissions in Sanity
Sanity roles and permissions control what each project member can read, edit, publish and configure. Every project has built-in roles: Administrator and Viewer on all plans, plus Editor, Developer and Contributor on Growth and above. You assign roles to members in sanity.io/manage or with the CLI, and the Content Lake enforces them on every request. Enterprise projects can also build custom roles with GROQ-filtered content resources, such as "can edit only article documents in the staging dataset".
Getting roles right matters for more than security. It keeps writers from accidentally changing project settings, lets reviewers read without costing a seat, and makes it clear who can publish. This guide covers the built-in roles and what each one grants, inviting and assigning members, how permissions combine, custom roles and content resources, role-based Studio customisation, and a sensible role plan for a typical team.
How Access Control Works in Sanity
Sanity's access model has four parts:
- Resources: the things access applies to, such as a project, an organisation, datasets and documents.
- Permissions: actions on a resource, such as reading documents, publishing or managing members.
- Roles: named bundles of permissions.
- Members: users (people) and robots (API tokens) that hold one or more roles.
Permissions are enforced by the Content Lake itself, not by the Studio. If a Contributor tries to publish through the API with their session, the request fails just as it would in the Studio. That's an important distinction we'll come back to when customising the Studio UI.
Built-In Roles by Plan
| Role | Plans | Content access | Project settings |
|---|---|---|---|
| Administrator | All | Read, write and publish in all datasets | Full, including members, tokens, CORS, webhooks and datasets |
| Viewer | All | Read-only in all datasets | None (read-only member list) |
| Editor | Growth, Enterprise | Read, write and publish in all datasets | Limited; can't create datasets |
| Developer | Growth, Enterprise | Read, write and publish in all datasets | Developer settings: datasets, tokens, CORS, webhooks, GraphQL, deploys |
| Contributor | Growth, Enterprise | Read and write drafts; can't publish | None |
| Custom | Enterprise | Whatever you define | Whatever you define |
A few details worth knowing:
- Free plans only have two roles: every member is either an Administrator or a Viewer. If you need people who can edit without managing the project, you need Growth. The Sanity pricing guide covers what else each plan includes.
- Viewers are free: the built-in Viewer role doesn't use a seat, so stakeholders who only need to read or comment cost nothing. Give a Viewer any additional role, though, and they become billable. A custom read-only role is also billable.
- Developers can't remove members: they can invite and update members, but deleting members and adding administrators is reserved for Administrators.
Contributor: The Role for Review Workflows
Contributor is the most useful role for editorial teams. Contributors can create and edit drafts but can't publish, so a writer can prepare an article and an Editor publishes it after review. Pair it with comments and tasks, and you have a basic approval workflow without custom code. The editorial workflows post builds on this.
Inviting Members and Assigning Roles
In Manage, open your project, go to Members and use Invite members. Choose the role when you invite, and change it later from the member list.
The CLI does the same:
# Invite a writer as a Contributor
npx sanity users invite writer@example.com --role contributor
# See who has access, including robots (tokens)
npx sanity users list --robots
Role names are lower-case in the CLI. Run npx sanity users invite --help to see the roles available on your plan.
Sign-In Methods Matter
A Sanity account belongs to a sign-in method, not just an email address. If you invite someone who accepts with Google but later signs in with GitHub using the same email, they arrive as a separate account with no roles. When someone says "I've lost access", check which method they used. On Enterprise, SSO can assign roles automatically from identity provider groups, which avoids this entirely.
How Permissions Combine
Permissions in Sanity are additive. A member's effective access is the union of every role they hold, and nothing can subtract a permission granted elsewhere.
That leads to two rules:
- You can't restrict with a second role. If someone holds Editor and a custom "articles only" role, they can still edit everything, because Editor grants access to all documents.
- Start narrow and add. When building custom roles, grant access dataset by dataset. A permission granted on "All datasets" overrides more restrictive settings on individual datasets.
There's one more gotcha: in a public dataset, every project member can read published content even if their role says "No access", because published content in a public dataset is readable by anyone. Use a private dataset if read access needs to be restricted.
Roles for API Tokens
Robots get roles too. When you create an API token, you choose its role, typically Viewer for read-only access (including drafts) or Editor for read and write. The same least-privilege thinking applies:
npx sanity tokens create "Website preview" --role=viewer
Treat tokens as members you have to manage: label them clearly, give each application its own, and delete them when they're no longer needed. The details of storing and rotating tokens are in securing your Sanity project.
Custom Roles and Content Resources
Custom roles are an Enterprise feature. They let you define exactly which content and settings a role can touch. You build them in Manage under Access › Roles, or with the Access API.
Management Permissions
Management permissions cover project settings: project details, members and roles, API settings and datasets. For any custom role to work in the Studio, set at least:
- Project details to read, or the role can't access the project.
- Project datasets to read, or dataset queries fail with a missing grant error.
- Project members to read, so presence and avatars work properly in the Studio.
Content Permissions and Resources
Content permissions are set per dataset (or per dataset tag) at three cumulative levels: Read, Update and create, and Publish, which adds deleting and publishing. The built-in resources are All documents, Image assets and File assets.
A content resource narrows those permissions with a GROQ filter. For example, a resource for articles only:
_type == "article"
Or articles in one section of a site:
_type == "article" && section == "sport"
Filters have limits. They can't dereference (use author._ref == "author-123" instead of author->), can't run subqueries and can't join documents. If you need to check something across documents, denormalise the value onto the document you're filtering.
Resources for Drafts and Releases
Drafts, release versions and releases are stored under fixed ID paths, so a custom role needs resources that match those paths:
| Studio feature | Resource filter | Access level |
|---|---|---|
| Work with drafts | _id in path("drafts.**") | Update and create |
| Edit documents inside a release | _id in path("versions.**") | Update and create |
| Create, schedule and publish releases | _id in path("_.releases.**") | Publish |
| Upload images and files | Built-in image and file asset resources | Update and create |
A frequent mistake is a role that can edit article documents but has no drafts resource, so editors can open articles and can't change anything.
Attribute-Based Roles
On plans with user attributes, filters can reference the current user. One "Regional editor" role can then cover every region:
_type == "article"
&& defined(region)
&& coalesce(user::attributes().region, "__none__") == region
The coalesce() guard matters. If the attribute is missing and the field is missing too, a naive region == user::attributes().region evaluates null == null, which is true, and quietly grants access to everything.
Tailor the Studio by Role
Roles decide what the API allows. The Studio also lets you shape what each user sees, which reduces clutter and mistakes. The currentUser object, with its roles array, is available in most configuration callbacks.
Hide Tools From Non-Administrators
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'
const isAdmin = (roles: { name: string }[] = []) =>
roles.some((role) => role.name === 'administrator' || role.name === 'developer')
export default defineConfig({
name: 'default',
title: 'My Site',
projectId: 'your-project-id',
dataset: 'production',
plugins: [structureTool(), visionTool()],
tools: (prev, { currentUser }) =>
isAdmin(currentUser?.roles) ? prev : prev.filter((tool) => tool.name !== 'vision'),
schema: { types: schemaTypes },
})
Remove Document Actions
You might want only administrators to delete documents:
// sanity.config.ts (inside defineConfig)
document: {
actions: (prev, { currentUser }) =>
isAdmin(currentUser?.roles)
? prev
: prev.filter((action) => action.action !== 'delete'),
},
The custom document actions post covers the actions API in depth.
Show Different Structure
// structure.ts
import type { StructureResolver } from 'sanity/structure'
export const structure: StructureResolver = (S, { currentUser }) => {
const isAdmin = currentUser?.roles.some((role) => role.name === 'administrator')
const items = [
S.documentTypeListItem('article').title('Articles'),
S.documentTypeListItem('author').title('Authors'),
]
if (isAdmin) {
items.push(S.divider(), S.documentTypeListItem('siteSettings').title('Site settings'))
}
return S.list().title('Content').items(items)
}
Pass it to structureTool({ structure }). The desk structure guide shows more patterns.
UI Is Not Security
Hiding something in the Studio doesn't stop the user from doing it. Someone with the Editor role can still delete documents through the API, Vision or a script, even if you've removed the delete button. Use Studio customisation to simplify the interface, and use roles (or custom roles on Enterprise) to enforce what people are allowed to do.
A Practical Role Plan
For a typical marketing site or publication on the Growth plan:
| Person | Role | Reason |
|---|---|---|
| Technical lead | Administrator | Owns members, access and project settings |
| Developers | Developer | Manage datasets, tokens, CORS and webhooks without full admin |
| Section editors | Editor | Review and publish |
| Writers and freelancers | Contributor | Draft without publishing |
| Stakeholders and legal reviewers | Viewer | Read and comment, no seat cost |
| Website | Viewer token | Reads drafts for preview only |
| Import scripts | Editor token, temporary | Deleted when the job is done |
Keep at least two Administrators so you're never locked out, review members when people leave, and remove freelancers at the end of their contracts. If you run several environments, the datasets and environments guide explains how to separate staging from production.
Troubleshooting Access Problems
| Symptom | Likely cause | Fix |
|---|---|---|
| A writer can't publish | They're a Contributor | Expected; an Editor publishes, or change their role |
| A user sees "missing required grant ... datasets/read" | Custom role without Project datasets read | Add the management permission |
| A custom role can open but not edit documents | No drafts resource | Add _id in path("drafts.**") with Update and create |
| A restricted user can still edit everything | They also hold Editor or another broad role | Remove the broader role |
| A new role isn't taking effect | Studio cached old permissions | Ask the user to reload the Studio |
| An invited user has no access | Signed in with a different method | Remove the stray account and re-invite |
FAQ: Sanity Roles and Permissions
Only Administrator and Viewer. Administrators can do everything, and Viewers have read-only access. Editor, Developer and Contributor need the Growth plan, and custom roles need Enterprise.
No. Members with only the built-in Viewer role don't use a seat on any plan. If you give a Viewer another role, or create a custom read-only role, that member becomes billable.
Not with the Editor role itself, because publishing is part of it. Assign Contributor instead, which allows editing drafts but not publishing. Finer control, such as publish rights for one document type only, requires custom roles on Enterprise.
Yes, on Enterprise. Create a content resource with a GROQ filter such as _type == "article", give a custom role access to it, and make sure the user doesn't also hold a broad role like Editor, because permissions are additive.
No. Studio customisation only changes the interface. The Content Lake enforces roles, so anything a user's role allows can still be done through the API or a script. Use roles to restrict access and Studio configuration to simplify the UI.
Create the token with the narrowest role that works, usually Viewer for reads or Editor for writes, and give each application its own token. On Enterprise you can assign a custom role to a token for tighter scopes.
Conclusion
Sanity's built-in roles cover most teams. Use Administrator sparingly, Developer for technical staff, Editor for people who publish, Contributor for writers who need review, and Viewer for anyone who only reads. Assign them in Manage or with sanity users invite, give tokens the same least-privilege treatment, and remember that permissions add up across every role a member holds.
When the built-in roles aren't enough, Enterprise custom roles and content resources let you scope access by dataset, document type or user attribute. Whatever plan you're on, shape the Studio by role to keep the interface focused, but rely on roles, not hidden buttons, to decide what people can actually do.


