👀 Our most exciting product launch yet 🚀 Join us May 8th for Sanity Connect
Last updated July 06, 2023

Customizing the Studio for Users and Roles

By Simeon Griggs & Rune Botten

The promise of an excellent Sanity Studio is that Developers craft uniquely useful experiences for Authors.

This guide covers how to customise some of the most commonly used parts of the Studio on a per-user or per-role basis.

Desk Structure

An Administrator in your Studio likely needs visibility across all of your content types. But an Author that is only concerned with a specific domain could be given a simpler experience with less options.

With Desk Structure configured in your Studio, we can subscribe to the userStore in order to find out details about the current user.

The example below will display different items to a user whose roles include one with the name "administrator".

// ./sanity.config.ts

import {visionTool} from '@sanity/vision'
import {defineConfig} from 'sanity'
import {deskTool, StructureBuilder} from 'sanity/desk'

import {schemaTypes} from './src/schemas'

const authorItems = (S: StructureBuilder) => [
  S.documentTypeListItem('post').title('Posts'),
  S.documentTypeListItem('author').title('Authors'),
]

const adminItems = (S: StructureBuilder) => [
  S.documentTypeListItem('project').title('Projects'),
  S.documentTypeListItem('setting').title('Settings'),
]

export default defineConfig({
  // ...other config options
  plugins: [
    // ...other plugins
    deskTool({
      structure: (S, {currentUser}) => {
        const isAdmin = currentUser?.roles.find((role) => role.name === 'administrator')

        const title = isAdmin ? 'Administrator Content' : 'Content'
        const items = isAdmin ? [...authorItems(S), S.divider(), ...adminItems(S)] : authorItems(S)

        return S.list().title(title).items(items)
      },
    }),
  ],
})

In addition to customizing which Desk Structure items are shown, we can customize what items are shown in a Document List based on GROQ's identity() function.

Protip

The GROQ function identity() returns the ID of the User performing the query. Super-useful in the Studio!

For example, perhaps instead of listing all author documents, you only want the Author to see and edit their own document.

S.listItem()
  .id('profile')
  .title('My Profile')
  .child(() =>
    S.documentList()
      .title('My Profile')
      .schemaType('author')
      .filter('_type == "author" && userId == identity()')
  ),

However, this will only work if your author documents contain a correct value in a userId field, let's fix that!

Gotcha

Hiding documents in Desk Structure will not remove them from search results in the Studio or in Reference fields. Apply additional filtering to those where possible.

Initial Values

Adding Sanity User ID's to documents can make them easier to query with GROQ using identity()

Using Initial Value Templates you can create individual templates to populate new documents with static or dynamic values.

Using a similar function to the one above, we can create a template for post type documents to pre-fill an author field with the User ID.

// ./sanity.config.ts

export default defineConfig({
  // ... all other config
   schema: {
    // ... schema types, etc
    
    templates: (prev, {currentUser}) => {
      // User is not guaranteed to be logged in
      if (!currentUser?.id) {
        return prev
      }

      // Create new template for new author type documents
      const authorWithUserId = {
        id: `author-with-user-id`,
        title: `Author with User ID`,
        schemaType: `author`,
        value: {
          userId: currentUser.id,
        },
      }

      // Remove the default template for author type documents
      const prevFiltered = prev.filter((template) => template.id !== `author`)

      return [...prevFiltered, authorWithUserId]
    },
  }
})

Next steps

  • Make the author field readOnly so the data is not corrupted
  • Use a plugin to visualize the User ID with an avatar and name

Comparing "Authors" to "Users"

It is possible, and likely, your schema will include something like an author type document. Or a greater abstraction; a person type document that can be assigned the value of "author" either as a string or reference.

These are different from Sanity Users – also known as Project Members – and that's fine! An author document likely contains much more details about that person than their Sanity User object does – like biography, contact and social details.

So instead of adding a User ID to every article, page, etc. Add it to a single author document which is connected to those types by a reference.

This way the User ID is only assigned to a single document, and a query like this will resolve all of their connected documents based on who is querying them:

*[_type in ["article", "page"] && author->userId == identity()]

Gotcha

An exception to this is you need to lock an entire document at the Permissions level based on a User ID, as Content Resources cannot resolve references. See the section on Permissions below.

Creating new documents

While we have hidden items of the Desk Structure from specific Users, they will still be able to create new documents of all types from the Navbar (next to the search box).

In the section above, we customized Initial Value Templates to remove the default new author document template. Here we can ensure globally that non-administrators cannot create new project or setting type documents.

// ./sanity.config.ts

export default defineConfig({
  // ... all other config

  document: {
    // ... all other document config
    
    newDocumentOptions: (prev, {currentUser}) => {
      // User is not guaranteed to be logged in
      if (!currentUser?.id) {
        return prev
      }

      const isAdmin = currentUser?.roles.find((role) => role.name === 'administrator')

      // Administrators can create any document type
      if (isAdmin) {
        return prev
      }

      // Remove the option to create new 'project' or 'setting' type documents
      const prevFiltered = prev.filter(
        (template) => !['project', 'setting'].includes(template.templateId)
      )

      return prevFiltered
    },
  },
})

Hidden and Read-only Document Fields

Fortunately the hidden and readOnly properties in schema have direct access to a currentUser variable.

Here we're hiding a price field to any user without the administrator role.

defineField({
  name: 'price',
  type: 'number',
  // Hide the price field from non-administrators
  hidden: ({currentUser}) => !currentUser?.roles.find((role) => role.name === 'administrator'),
})

readOnly has the same currentUser property, and it can even be added at the document level to set all fields to readOnly.

In this example, we'll lock the document to any user that is not an administrator or editor.

export default defineType({
  name: 'project',
  title: 'Project',
  type: 'document',
  readOnly: ({currentUser}) => !currentUser?.roles.find((role) => role.name === 'administrator'),
  fields: [ // ...all your fields ]
})

Protip

Setting a field hidden will not remove its value from the Inspect panel, so Users will still be able to see the value of the field.

Making fields readOnly will only protect the field in the Studio, a sufficiently motivated User could still write to the field with an API call.

The only true way to secure these fields and documents is with access control.

Document Actions

Sanity's Roles and Permissions API will ensure that built-in Document Actions like Publish or Delete cannot be clicked by Users that lack those permissions.

But for your custom Actions, you'll need to inspect the current user to hide or disable the Action selectively.

Fortunately, the Studio provides a useCurrentUser hook, which can be used inside a Document Action as they are rendered inside of React scope.

Here's a code example that disables the action for non-administrators.

import {DocumentActionProps, useCurrentUser} from 'sanity'

export default function CreateMilestone(props: DocumentActionProps) {
  const user = useCurrentUser()
  const isAdmin = user ? user.roles.find((role) => role.name === 'administrator') : false

  return {
    // Disable the button for non-"administrator" users
    disabled: !isAdmin,
    label: 'Create Milestone',
    onHandle: async () => {
      // ...your function logic
      props.onComplete()
    },
  }
}

You could also:

  • return null to hide the action from non-administrators or
  • filter out the Action based on the current user inside sanity.config.ts in the document.actions configuration

Custom Roles and Permissions

On Enterprise plans, access to view and edit documents in the Studio experience can be automatically be customized thanks to Content Resources.

Going back to our userId the example earlier, it's possible to write a rule that would only give access to users editing an author document other than the one with their userId with the following rule:

_type == "article" && userId == identity()

Because Content Resources cannot dereference fields, this userId would also need to be present on any document type you wish to restrict access to in the same way.

The same logic can filter document lists, search and reference fields at the permission level. By restricting view access to documents that do not have your userId.

Gotcha

All Published documents are viewable in a Public Dataset. If you're trying to restrict the ability to View documents using Permissions, you must also switch to a Private Dataset.

Conclusion

Configuring every part of the Studio into a unique experience for specific user groups allows you to place gentle guardrails around authors to ensure they have the most guided and pleasant experience possible.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by authors

Create richer array item previews

Object types use a preview property to display contextual information about an item when they are inside of an array; customizing the preview component can make them even more useful for content creators.

Simeon Griggs
Go to Create richer array item previews