Discussion about creating custom roles and permissions in Sanity for a non-profit client.
I totally understand your frustration - this is one of the most common pain points I see people hit with Sanity. The good news is there are several workarounds you can use to achieve role-based permissions without the Enterprise plan, though they require a bit more custom code.
What's Available on Different Plans
You're right that custom roles are limited to Enterprise. On the Growth plan ($99/month), you get:
- Up to 5 default role assignments (Administrator, Developer, Editor, Viewer, Contributor)
- Up to 50 user seats
These default roles are fairly broad, so they won't give you the granular control your client needs out of the box.
Practical Workarounds
Here are the approaches I'd recommend for your use case:
1. Hide UI Elements Based on Role Using Structure Builder
You can customize the Studio UI to hide document types, actions, and navigation items based on the current user's role. This doesn't enforce permissions at the API level, but it prevents users from accessing things they shouldn't in the Studio interface.
The Structure Builder API provides access to currentUser in its context, which you can use to filter what appears:
import type {StructureResolver} from 'sanity/structure'
export const structure: StructureResolver = (S, context) => {
const user = context.currentUser
const isAdmin = user?.roles.some(role => role.name === 'administrator')
return S.list()
.title('Content')
.items([
S.documentTypeListItem('page').title('Pages'),
// Only show navigation to admins and editors
...(isAdmin || user?.roles.some(r => r.name === 'editor')
? [S.documentTypeListItem('navigation').title('Navigation')]
: [])
])
}2. Customize Document Actions
You can customize document actions to control who can publish based on their role. This is done in your sanity.config.ts:
import {defineConfig} from 'sanity'
export default defineConfig({
// ... other config
document: {
actions: (prev, context) => {
const {currentUser, schemaType} = context
const isAuthor = currentUser?.roles.some(r => r.name === 'editor')
// Authors can't publish navigation
if (schemaType === 'navigation' && isAuthor) {
return prev.filter(action => action.action !== 'publish')
}
return prev
}
}
})3. Control Document Creation with Initial Value Templates
You can hide or show document creation templates based on user roles. The Studio customizations lesson shows how to control both initial value templates and the "Create +" menu:
export default defineConfig({
// ... other config
schema: {
types: schemaTypes,
templates: (prev, context) => {
const {currentUser} = context
const isAuthor = currentUser?.roles.some(r => r.name === 'editor')
if (isAuthor) {
// Filter out page creation for authors
return prev.filter(template => template.id !== 'page')
}
return prev
}
},
document: {
newDocumentOptions: (prev, {currentUser}) => {
const isAuthor = currentUser?.roles.some(r => r.name === 'editor')
if (isAuthor) {
// Remove page from the global create menu
return prev.filter(item => item.templateId !== 'page')
}
return prev
}
}
})4. Use Conditional Fields
You can make fields hidden or read-only based on role within your schema:
defineField({
name: 'slug',
type: 'slug',
readOnly: (context) => {
const isAdmin = context.currentUser?.roles.some(r => r.name === 'administrator')
return !isAdmin
}
})About Workflows
The Workflows plugin you mentioned is more about content review and approval processes rather than restricting access to specific content areas. It won't solve your role-based access control needs, though it could complement the above approaches for editorial workflows.
Important Limitations
These workarounds have a key limitation: they only restrict the Studio UI, not the API. A technically savvy user could still use the API directly to edit restricted content. For most clients (especially non-profits with trusted staff), this is an acceptable trade-off. True API-level permission enforcement with GROQ filters and content resources requires Enterprise.
My Recommendation
For your non-profit client, I'd implement a combination of the approaches above:
- Use Structure Builder to hide navigation from Authors
- Customize document actions to remove publish capabilities where needed
- Control document creation through templates and newDocumentOptions
- Use conditional fields for additional granularity
This will give you:
- Administrators who see and can edit everything
- Editors who can create/edit/publish most documents including navigation
- Authors with limited UI access to specific document types, no navigation access, and restricted page creation
The implementation takes some custom code, but it's totally doable and will give your client the experience they need within their budget. The Studio customizations lesson has comprehensive examples of these patterns in action, including filtering document lists, customizing actions, and controlling document creation.
If the client absolutely requires API-level security (for example, if they have untrusted users or compliance requirements), then you'd need to either advocate for Enterprise pricing or consider whether Sanity is the right fit for this specific project. But for most real-world scenarios with trusted team members, the UI-level restrictions work great and are widely used in production.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.