# Course: Users, roles and using roles
https://www.sanity.io/learn/course/introduction-to-users-and-roles

Core concepts around setting up custom access roles and permissions. Help editors work faster by configuring your Studio to provide role-based customizations.

---

## Navigation

## Contents

1. [Introduction](https://www.sanity.io/learn/course/introduction-to-users-and-roles/introduction) · [markdown](https://www.sanity.io/learn/course/introduction-to-users-and-roles/introduction.md)
2. [Typical use cases](https://www.sanity.io/learn/course/introduction-to-users-and-roles/typical-use-cases) · [markdown](https://www.sanity.io/learn/course/introduction-to-users-and-roles/typical-use-cases.md)
3. [Custom roles and resources](https://www.sanity.io/learn/course/introduction-to-users-and-roles/custom-roles-and-resources) · [markdown](https://www.sanity.io/learn/course/introduction-to-users-and-roles/custom-roles-and-resources.md)
4. [Defining roles](https://www.sanity.io/learn/course/introduction-to-users-and-roles/defining-roles) · [markdown](https://www.sanity.io/learn/course/introduction-to-users-and-roles/defining-roles.md)
5. [Studio customizations](https://www.sanity.io/learn/course/introduction-to-users-and-roles/studio-customizations) · [markdown](https://www.sanity.io/learn/course/introduction-to-users-and-roles/studio-customizations.md)
6. [Roles quiz](https://www.sanity.io/learn/course/introduction-to-users-and-roles/roles-quiz) · [markdown](https://www.sanity.io/learn/course/introduction-to-users-and-roles/roles-quiz.md)

---

## Lesson 1: Introduction
https://www.sanity.io/learn/course/introduction-to-users-and-roles/introduction

Why – and how – to use custom roles to deliver effective workflows and tailored user experiences.

In content operations, being able to effectively handle users and roles is hugely important – this is even more apparent when you scale content operations across a large organization using a content operating system such as Sanity.



> [!WARNING]
> This course covers the **custom roles** features available exclusively on Sanity’s Enterprise plans. That said, some elements of this course like [Studio Customizations](https://www.sanity.io/learn/course/introduction-to-users-and-roles/studio-customizations) could be applied on a per-user level on all plans.



In this course, we’ll introduce core concepts of the Sanity platform for setting up custom roles, as well as getting hands-on with some example scenarios and Studio customizations.



You will learn the following:



- The common reasons for setting up users and roles

- How to set up and configure custom roles and resources in Sanity to meet your unique requirements

- How you can customize your Studio with role-based customizations to give your editors a tailored experience, enabling them to work faster

> [!NOTE]
> For enterprise customers, this will typically be followed up with an onboarding workshop led by your Solution Architect to help with the practical application of concepts from this course.



## Recommended reading



This course makes an assumption you have a baseline knowledge of Sanity. If you’re new to Sanity, then the following courses are recommended before progressing:



> [!TIP]
> [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio)


> [!TIP]
> [Studio excellence](https://www.sanity.io/learn/course/studio-excellence)


> [!TIP]
> [Between GROQ and a hard place](https://www.sanity.io/learn/course/between-groq-and-a-hard-place)



---

## Lesson 2: Typical use cases
https://www.sanity.io/learn/course/introduction-to-users-and-roles/typical-use-cases

Motivations for setting up custom roles and permissions in content operations

The most common requirements for configuration of users and roles we see are:



- Security

- Content integrity and compliance

- Workflow management

- Localization

- Scalability & maintenance

- Customizing the user experience


## Security



This could cover ensuring that only certain users have access to view or update some sensitive content (such as embargoed press releases) and would typically involve defining what actions a user can perform – from full publishing control through to not being able to view the content at all.



> [!NOTE]
> If content privacy is crucial, consider [making your dataset private](https://www.sanity.io/docs/keeping-your-data-safe#5c2e941ea03c). Sanity datasets are public by default, meaning **published** content can be queried from anywhere using your public API endpoint.



## Content integrity and compliance



In some industries such as the financial and legal sector, accuracy of content is critical. Preventing errors being published is therefore of utmost importance. Configuring roles and permissions to limit who can create, modify or publish information can reduce – or even remove entirely – the risk of inexperienced users publishing incorrect information.



> [!NOTE]
> Read more about our [History Experience](https://www.sanity.io/learn/docs/user-guides/history-experience) to see how Sanity keeps a record of all changes to content – and who made them. Combined with users and roles, you can create fully accountable and controlled content workflows.



## Workflow management



Sometimes it’s necessary for users to have different responsibilities. For instance, a writer could draft content, an editor could review it, and a publisher could review and publish it. Role-specific permissions can streamline and guarantee an effective process.



## Localization



When you need to publish content in a business serving multiple international markets, it can be useful to control rights for content that belongs to a particular geographic locale. This could be for different translators who speak different languages, or different legal teams who need to ensure compliance in different jurisdictions.



> [!TIP]
> Read [Localization](https://www.sanity.io/learn/studio/localization) in the documentation for general guidance around configuring localized content with Sanity



## Scalability and maintenance



As organizations grow, managing users through roles becomes more efficient - new users can be quickly assigned roles that define their permissions. By controlling what access is given to users, it can become easier to train new users and maintain the system.



## Customizing the user experience



If users have certain content that aligns with their responsibilities or interests based on their role, customizing the Sanity Studio to show only content that is relevant for them can reduce complexity and improve their overall user experience.



It might also be relevant to customize the tools, plugins and dashboards available to certain users. For example, a dashboard could show all documents awaiting approval by a publisher – but this might not be relevant for a writer.



> [!TIP]
> The [Member-specific options](https://www.sanity.io/learn/course/studio-excellence/member-mastery) lesson in the [Studio excellence](https://www.sanity.io/learn/course/studio-excellence) course shows several examples of how the Sanity Studio experience can be modified for a user.



---

## Lesson 3: Custom roles and resources
https://www.sanity.io/learn/course/introduction-to-users-and-roles/custom-roles-and-resources

Set up content resources and roles to meet your requirements around security, compliance, workflows and user experience 

## Default roles 



Each plan has a number of [default roles](https://www.sanity.io/docs/roles#e2daad192df9). These have predefined permissions which are applied to all datasets within the project. They also have default project-level permissions, such as which roles can create API tokens for the project.



Whilst these default roles cover many common role-based workflows (such as draft → review → publish), for many of the use cases above, it’s necessary to configure custom roles.



It’s also important to note that users can have *multiple *roles. This is particularly useful in combining custom roles to create unique combinations of permissions.



## Custom roles



Core to being able customize content operations to meet your requirements around security, compliance, workflow and user experience are *custom roles.*



> [!TIP]
> See our documentation on [Roles](https://www.sanity.io/learn/user-guides/roles), all roles can be configured at [sanity.io/manage](https://www.sanity.io/manage)



Custom roles are made up of two key elements:



- **Management permissions:** control over the changes a role can make to project settings - like API/webhook configuration, dataset management and user access.

- **Content permissions:** control over which roles have permissions to make changes to certain content *resources*. These content permissions can be granted across *all *datasets*,* a group of *tagged *datasets, or an *individual *dataset.

> [!WARNING]
> Custom roles, content resources, and user attributes are enterprise-only features. If you're on a different plan, this lesson is still worth reading to understand what's possible, but you'll need an enterprise plan to configure these in your project.


> [!WARNING]
> When configuring custom roles, you may need to assign certain permissions for handling generation of preview tokens. See the [Visual Editing readme](https://github.com/sanity-io/visual-editing/blob/main/packages/preview-url-secret/README.md#permissions-model) for the latest information.



### Dataset privacy



In many scenarios with custom roles, it may be that requirements involve removing the ability for a user to *see* content. This means that it might be necessary to [set the dataset to private](https://www.sanity.io/docs/keeping-your-data-safe#5c2e941ea03c). In a public dataset, all documents are readable by all users regardless of authentication. That means documents you may want to hide will still show up in the Studio search as well as in public API calls (when published).



If in doubt, it's safest to make your dataset private. Just remember that your front-ends will then need to make authenticated calls and you'll need to consider [securing your API token](https://www.sanity.io/docs/http-auth#504058b73b71).



> [!WARNING]
> When removing access to certain content, it's important to remember that default roles grant access to *everything* in a dataset, rather than scoped access. Combining roles without consideration could unintentionally give incorrect access levels.



## Content resources



Whilst the default roles apply to *all* content in a dataset, custom roles support applying permissions to a *subset* of content in a dataset. This is done by creating content resources.



> [!NOTE]
> **Content resources** are essentially a set of documents in a dataset, defined by a GROQ filter.



This provides a high level of flexibility to assign permissions not just to particular document types, but to filtered scopes, too.



Let’s consider a few examples based on the [common use cases](https://www.sanity.io/learn/course/introduction-to-users-and-roles/typical-use-cases) we outlined earlier.



### Example - Embargoed Articles



Let’s say we have a document type called “article”, which our *editorial* team want to lock down to prevent edits from users in our *merchandising* team that should only manage our product catalogue.



A GROQ filter could create an “Article” content resource:



```groq:Article Content Resource
_type == "article"
```

Below shows how this configuration would look at [sanity.io/manage](https://www.sanity.io/manage):



![A custom content resource targeting article documents](https://cdn.sanity.io/images/3do82whm/next/0a425bbe7dbfde3081f915d25e1666db602d5e08-1609x1620.png)

Using this, we could create a role to target all article types. However, if our editorial team wanted to add further permissions to ensure that embargoed content could be seen only by *managers* in the *editorial* team, they could add an “embargoed” boolean field to their schema and create an “Embargoed Article” resource:



```groq:Embargoed Article Content Resource
_type == "article" && embargoed == true
```

Consider creating a “Non-Embargoed Article” content resource to explicitly *exclude* the embargoed articles. A grant simply on “Article” would include all articles. This illustrates a key concept that roles are additive.



```groq:Non-Embargoed Article Content Resource
_type == "article" && embargoed != true
```

Using these content resources, we could create new roles to assign these content types to our users, which might look something like:



#### Role: Creator Team



- **Article:** No Access

- **Embargoed Article:** No Access

- **Unembargoed Article:** Publish

- **Product:** Publish


#### Role: Article Editor



- **Article:** Publish

- **Embargoed Article:** Publish

- **Product:** Publish


Technically, the “Embargoed Article” permission is not needed as the simpler “Article” resource gives publish access to *all* articles. However it can be good to positively add this as a future-facing permission - this also ensures visibility of the non-embargoed articles.



> [!NOTE]
> When embargoing content, you might also want to consider assets. Whilst [asset documents](https://www.sanity.io/docs/assets#2cee91f4f62d) can be hidden from API calls with roles, the direct URL of the asset itself is not authenticated. Usually the autogenerated URL of the asset provides enough security through obscurity – but the file itself *is* publicly accessible, even in private datasets.



### Example - Legal Policies



It might be that you have legal documents stored within your dataset, and want to restrict the ability of users not in your legal team from making changes to these documents.



In this case, you might create a “Legal Policy” content resource:



```groq:Legal Policy Content Resource
_type == "policy"
```

Let’s say we want to make it so each lawyer in our team is ultimately responsible for the documents they create. In this scenario, we might have a field on our document which declares the user ID of the user that created our document. We could create a “My Policies” content resource:



```groq:My Policies Content Resource
_type == "policy" && createdBy == identity()
```

In this case, we can see how we can use [GROQ functions](https://www.sanity.io/learn/docs/specifications/groq-functions) in the context of our content resources.



> [!NOTE]
> The `createdBy` field above isn’t a system field - it must be added to your Sanity Studio schema, and populated with an initial value. More on this in the [Studio Customizations](https://www.sanity.io/learn/course/introduction-to-users-and-roles/studio-customizations) lesson of this course.



Now we could create a single role to ensure lawyers can see and edit all policies, but only publish their own:



#### Role: Legal Team



- **Legal Policy:** Update and Create

- **My Policies:** Publish


### Example - Locales



In this example, we’ll cover a couple of scenarios. One in which you have a multilingual setup and want to restrict access to documents of a particular language. The other is where documents might belong to a particular location - let’s say a particular store.



#### Languages



Let’s create an “English Document” content resource which will cover all documents in English:



```groq:English Document Content Resource
language == 'en' // this could be 'en-gb' or 'en-us'
```

You might notice in this case we don’t add a type, as we want this content resource to simply look at the language field and control access for *all* English content, regardless of its `_type`.



#### Locations



For the store example, imagine a `store` document type, with each document representing a store location. Each store needs to have a number of document types associated with it - we have types for `offer`, `person` and `product`. Offers and people belong to a single store, but a product can belong to many stores - therefore offers and people have a single `reference` field called `store` whereas `product` has an `array` of `references` called `stores[]` .



In this scenario, you might assume to create a “Tom’s Toy Store Manager” role you can follow the reference in the content resource GROQ query… but this won’t work:



```groq:Tom’s Toy Store Content Resoure
// This won't work...
store->name == "Tom's Toy Store" || "Tom's Toy Store" in store[]->name
```

This won’t work because content resources can only be based on values within a document, and therefore cannot resolve references. 



Instead you’ll need to know the ID of the document for Tom’s Toy Store and use this in the query instead:



```groq:Tom’s Toy Store Content Resource
// This will work
store._ref == "toms-toy-store-id" || "toms-toy-store-id" in store[]._ref
```

With the latter, you could create the “Tom’s Toy Store” content resource, and then apply it to your “Tom’s Toy Store Manager” role as necessary.



### User attributes



The examples above work well, but they require a separate role for each value — one per language, one per store location, and so on. If you're on an enterprise plan, user attributes give you a way around this.



User attributes are key-value pairs that describe a member of your organization — things like `language="en"`, `branch="london"`, or `department="legal"`. They can come from your identity provider via SAML (synced automatically on each login) or be set manually in Manage.



You can reference these attributes directly in content resource GROQ filters using `user::attributes()`. A user with `language="en"` will only see English content; a user with `language="fr"` will only see French content. One role, no duplication.



```groq:Language Content Resource
language == user::attributes().language
```

- User attributes are only available on enterprise plans. See the [Roles documentation](https://www.sanity.io/docs/user-guides/roles#c0c40b488537) for details on setting up and managing attributes.

- If a user is missing an attribute referenced in a content resource filter, the filter fails closed — they'll have no access to that content. Make sure all relevant users have the right attribute values set before assigning them roles that use parameterized resources.


---

## Lesson 4: Defining roles
https://www.sanity.io/learn/course/introduction-to-users-and-roles/defining-roles

Combine your resources with permission levels to define which roles can perform which actions

When it comes to creating custom roles, it’s a case of combining your shiny new content resources with permission levels - basically defining the rules “*this role* has *this level* of access to *this resource*”.



When defining roles and resources, there are a couple of key decisions to be made:



- Will your roles be wide ranging and attribute a number of permissions to a number of content resources?

- Will your roles be very precise with users assigned multiple roles to cover their required access levels?


These types of decisions are subject to your specific use case, and for our Enterprise customers we’d recommend workshopping these with your Solution Architect.



## Dataset Permissions



Custom roles can have permissions applied to all datasets or to specific datasets. Often, adding permissions for all datasets will be perfectly acceptable – but if you have specific workflows or a more complex dataset configuration it can be useful to tailor permissions for each dataset.



An example is creating a custom **developer** role whereby developers can create content in a **development** dataset but not in a **production** dataset.



If you have a more complex project with many datasets – for example a multi-brand configuration where each brand has a number of datasets – then using [dataset tags](https://www.sanity.io/docs/roles#0db30012bd04) can be very helpful. You can tag each dataset with the brand it belongs to and grant access to all those tagged datasets in a single role definition. Don’t forget datasets can have multiple tags, too.



## Roles are additive



This means you can’t remove a permission given to a user in one role by removing it on another role.



As an example, if you were to assign the default **Editor** role to a user, this role includes the **Publish** permission for **All documents** in **All datasets.** If you were then to give this same user the **Creator Team** role from our first example above – which has **Read** only permissions for articles – they would still be able to publish the **Article** and **Embargoed Article** content resources as a result of the **Editor** role.



## Roles and the API



One thing to remember is that roles – including custom roles – can be applied to the [API tokens](https://www.sanity.io/docs/http-auth#4c21d7b829fe) that you generate, too.



This can be really helpful if you need to restrict the types of content that can be written to by middleware, or with particular cases where you might want to give third parties controlled access via the API.



## Role Mapping with SAML SSO



If your organization has SAML SSO configured with Sanity to enable single sign-on – for example with Azure AD, Okta or another Identity Provider (IdP) – then you may benefit from [role mapping](https://www.sanity.io/docs/sso-saml#647d8f0f9ee4) to sync user roles in your IdP to user roles in Sanity. 



Particularly for projects with many users, this can be a real time saver!



## Customized permissions



You might notice when applying permissions to roles via the user interface at [sanity.io/manage](http://sanity.io/manage) you are restricted in the types of permissions you can create. These are baseline assumptions about the types of permission levels needed based on common practices. These allow:



- **No access** - no access at all (except with public datasets which are publicly readable)

- **Read** - read only

- **Update and Create** - create, read and edit

- **Publish** - create, read, edit, delete and publish/unpublish


What if you want to have a unique permission that grants the ability to *delete but not create,* or to *create but not read*? These are rare requirements, but in these cases specific permissions can be created via the [Roles API](https://www.sanity.io/docs/roles-reference) rather than the UI.



---

## Lesson 5: Studio customizations
https://www.sanity.io/learn/course/introduction-to-users-and-roles/studio-customizations

Change the user experience of the Sanity Studio based on roles and deliver a personalized user experience to accelerate editor workflows

When creating roles, it can be a great next step to change the Sanity Studio and customize the experience of your users based on their role.



Studio customizations might include:



- Showing, hiding or filtering certain content types using the [Structure Builder API](https://www.sanity.io/learn/docs/studio/structure-builder-introduction)

- Automatically populating initial values based on a user or role

- Making a field hidden or readonly based on a user or role

- Initializing different plugins / configuration based on a user or role

- Using a role to introduce or adjust a custom component

- Changing the available document actions or ability to create new documents based on a role

- Enabling / disabling [workspaces](https://www.sanity.io/docs/workspaces) based on a role. *There are some caveats to this, covered in the module below.*


## Example Scenario



For each of the above points – with the exception of role-based workspaces – we will introduce customizations based on a [reference Github repository](https://github.com/thebiggianthead/sanity-roles-workshop-demo)



The concept for this lesson is:



> Our organization has a number of stores across a number of cities. Each store has offers which are unique to the store. Each of the stores has a manager who should only be able to view, edit and publish offers for their own store. Regional managers can be assigned to multiple stores to manage offers, and administrators can manage all offers across all stores.   
  
Additionally, we publish articles – however, only admins can see all articles. Other users should only be able to work on articles they have created themselves.



### Intended outcomes



In this exercise, we’ll be demonstrating a few example customizations to a Studio – the intention of this is to *inspire ideas* as to how you might customize your own Sanity Studio(s) to meet your own unique requirements.



### Initial configuration



To follow along with this scenario you can either take the schema directly from the reference repository or create your own schema.



- [ ] Create an **offer** and **article** schema type

- [ ] Ensure the **offer** type has a **store**, as illustrated below


```typescript:src/schemaTypes/offer.ts
// define your stores
type Store = {
  id: string
  name: string
}

const stores: Store[] = [
  {
    id: 'store-1',
    name: 'Store 1',
  },
  {
    id: 'store-2',
    name: 'Store 2',
  },
]

// ... rest of offer definition
defineField({
  name: 'store',
  title: 'Store',
  type: 'string',
  options: {
    list: stores.map((store) => {
      return {
        value: store.id,
        title: store.name,
      }
    }),
    layout: 'radio',
  },
})
```

- [ ] Create the below **content resources** and user **roles**


#### Content Resource: Store 1



- **Title:** Store 1

- **Identifier:** store-1

- **GROQ filter:** `store == "store-1"`


#### Content Resource: Store 2



- **Title:** Store 2

- **Identifier:** store-2

- **GROQ filter:** `store == "store-2"`


#### Content Resource: User Articles



- **Title:** User Articles

- **Identifier:** user-articles

- **GROQ filter:** `_type == "article" && (createdBy == identity() || createdBy == $identity)`

> [!NOTE]
> Why `identity()` and `$identity`? This covers all versions of the API and Studio, so will make you a little more bulletproof. `$identity` may be removed entirely in a future version of the API.



#### Role: Store 1 Manager



- **Title:** Store 1 Manager

- **Identifier:** store-1-manager

- **Permissions in all datasets:** 

- Store 1 - Publish

- Image / file assets - Update and create

- All other resources - No access


#### Role: Store 2 Manager



- **Title:** Store 2 Manager

- **Identifier:** store-2-manager

- **Permissions in all datasets:** 

- Store 2 - Publish

- Image / file assets - Update and create

- All other resources - No access


#### Role: Article Editor



- **Title:** Article Editor

- **Identifier:** article-editor

- **Permissions in all datasets:** 

- User Articles - Publish

- Image / file assets - Update and create

- All other resources - No access


## How to test Studio customizations



The simplest way to test out the role-based customizations you make is to simply have a number of user accounts to switch between in different browser profiles / incognito windows. This way you can have a admin user and easily make changes to your secondary users' roles to test out the changes to the Studio as you make them in a local development environment. It’s simple and effective.



> [!WARNING]
> It's important to bear in mind that if you change your user to remove the administrator role, you might not be able to change it back.



## Customizing with User Context



To customize the Studio based on user and role, it’s necessary to know information about the current user. Thankfully, the Studio provides this context in a number of places including – but not limited to – the Structure Builder API, the Tool API, the Document Actions API and hidden / readonly callback functions.



Where this is available, the context will provide the `currentUser` object:



```typescript:CurrentUser type definition
interface CurrentUser {
  email: string
  id: string
  name: string
  profileImage?: string
  provider?: string
  role: string // deprecated, use roles instead
  roles: Role[]
}

// And for reference, the Role:
interface Role {
  name: string
  title: string
  description?: string
}
```

Inside React components or custom hooks, you can use the `useCurrentUser()` hook to return the same data.



There’s also the `userHasRole()` helper function to determine if a particular user has a provided role - this accepts a user object as it’s first argument and a role identifier string as it’s second.



> [!NOTE]
> Users can have multiple roles – it’s important to consider this in your customizations and role checks.



## Structure that makes sense



Customizing the content types a user sees – and how they see them – can shorten the user journeys in a Studio, greatly improving the overall Studio experience. Let’s expand on some of the principles established in the [Structure customization](https://www.sanity.io/learn/course/studio-excellence/structure-customization) lesson in the [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio) course.



This initial lesson on the Structure Builder API focused on the `StructureBuilder` object which the `StructureResolver` returns as it’s first argument. In order to customize this based on users and roles the second argument can be used – the `StructureResolverContext`.



This `context` object returns a number of useful things in addition to the user – like the `getClient` function which can be used to query your dataset(s). The key for customizing based on users is the `currentUser` object this context provides. Using this, it's possible to change the Structure for different users and roles.



### User specific articles



In the scenario outlined above, one of the steps required is to hide articles from the user if they didn’t create them. If all articles are listed, then users may end up seeing articles they can’t do anything with. This isn’t a great user experience:



![All articles - including those the user can't edit](https://cdn.sanity.io/images/3do82whm/next/4b41ebad300ef093accaeeb9bd83089fe6a1624c-2528x1660.png)

Instead, it's better to hide articles the user can’t edit – which declutters the Studio, and displays only the articles the user is able to work with:



![Filtered articles - showing only those the user can edit](https://cdn.sanity.io/images/3do82whm/next/aaeb3a12acb16acc2d8e52bfa9762fe2cad13bae-2528x1660.png)

To achieve this, we need to do a couple of things. Firstly, the `createdBy` field in our article document needs to be populated – this isn’t a system field.



- [ ] Add a `createdBy` field with an `initialValue` to the article schema type


```typescript:src/schemaTypes/article.ts
defineField({
  name: 'createdBy',
  title: 'Created By',
  type: 'string',
  initialValue: (param, context) => context.currentUser?.id || '',
  readOnly: (context) =>
    !context.currentUser?.roles.flatMap((r) => r.name).includes('administrator'),
})
```

Note that the field is also made `readOnly` for users that aren’t administrators - meaning nobody but admins can change the creator of a document. This could also be hidden.



> [!NOTE]
> Another approach for user scoped documents is to create an array of users - perhaps an `allowedUsers` array field. This scales to allow multiple users to access a document.


> [!NOTE]
> **Good to know **– If you prefer to choose from a list of users for the `createdBy` field, then the `<UserSelectMenu>` component from [`sanity-plugin-utils`](https://github.com/SimeonGriggs/sanity-plugin-utils) makes for a nice user experience.


> [!WARNING]
> **Gotcha** – initial values are only applied at the time of document creation. If you’re adding them retrospectively, you’ll need to patch the user values to pre-existing documents. The [Handling schema changes confidently](https://www.sanity.io/learn/course/handling-schema-changes-confidently) course covers handling data migrations / modifications like this.



Following the addition of this field, we can make use of the `StructureResolverContext` to make adjustments in the Structure of our Studio.



- [ ] **Update** your Structure to the code below to create your filtered list of articles


```typescript:src/structure/index.tsx
import {DocumentsIcon} from '@sanity/icons'
import type {ConfigContext} from 'sanity'
import type {
  DocumentListBuilder,
  StructureBuilder,
  StructureResolver,
} from 'sanity/structure'

const API_VERSION = '2023-01-01'

function defineStructure<StructureType>(
  factory: (S: StructureBuilder, context: ConfigContext) => StructureType,
) {
  return factory
}

export const structure: StructureResolver = (S, context) =>
  S.list()
    .id('root')
    .title('Content')
    .items([
      S.listItem()
        .title('Articles')
        .icon(DocumentsIcon)
        .schemaType('article')
        .child(createArticleList(S, context)),
      // other structure items...
    ])
    
const createArticleList = defineStructure<DocumentListBuilder>((S, context) => {
  const user = context?.currentUser
  const roles = user?.roles.map((r) => r.name)
  const isLimited = roles?.includes('article-editor')

  let userQuery = ``
  if (isLimited) {
    userQuery = `createdBy == $userId`
  } else {
    userQuery = ``
  }

  return S.documentTypeList('article')
    .title(`Articles`)
    .filter([`_type == "article"`, userQuery].filter(Boolean).join(` && `))
    .params({userId: user?.id})
    .apiVersion(API_VERSION)
})
```

What's going on here?



Firstly, a `defineStructure()` factory function helps Typescript to determine the types we expect for the various customizations being made.



In the Structure, the list item child for articles is passed a `createArticleList()` function - this grabs the user from the context and maps their roles into an array of strings. These roles can then be tested to see if the `article-editor` role is present.



In the case where this role is present, the article list should be limited. Therefore a filter is applied to the `documentTypeList` by joining `createdBy == $userId` to our query and passing the user ID as a parameter.



### User specific stores



Another requirement is to only show relevant store offers in the Structure. Essentially, the requirement is to show offers only when a user is a store manager, a regional manager or an administrator.



If I am none of these, I don’t want to see anything concerning stores or offers in the Structure in order to remove clutter and improve my experience:



![No stores shown if I am not a store manager or admin](https://cdn.sanity.io/images/3do82whm/next/0559daca7e69c61ff606e95054ade929d743d166-2528x1660.png)

If I am the manager of a single store – “Store 1” – then I’m only interested in offers for my particular store:



![A single store shown if I manage a single store](https://cdn.sanity.io/images/3do82whm/next/139cd026334dae98357acb1a9ec80041189ae9cb-2528x1660.png)

However, if I manage multiple stores, I want to be able to access offers for all of these stores:



![Multiple stores shown if I manage multiple stores](https://cdn.sanity.io/images/3do82whm/next/290a7686f6ccd0ccc29747bac3fbb477efe7495a-2528x1660.png)

Note that in the case of managing a single store, the item is at the *top-level* of the Structure - whereas when I manage multiple, these are nested under an “Offers” list item.



- [ ] **Update** your Structure to the code below to add offers to your structure


```typescript:src/structure/index.tsx
import {DocumentsIcon, HomeIcon, TagIcon} from '@sanity/icons'
import type {ConfigContext} from 'sanity'
import type {
  DocumentListBuilder,
  ListItemBuilder,
  StructureBuilder,
  StructureResolver,
} from 'sanity/structure'

import {stores} from '../lib/constants'

const API_VERSION = '2023-01-01'

function defineStructure<StructureType>(
  factory: (S: StructureBuilder, context: ConfigContext) => StructureType,
) {
  return factory
}

export const structure: StructureResolver = (S, context) =>
  S.list()
    .id('root')
    .title('Content')
    .items([
      S.listItem()
        .title('Articles')
        .icon(DocumentsIcon)
        .schemaType('article')
        .child(createArticleList(S, context)),
      ...[createOffers(S, context) as ListItemBuilder].filter(Boolean),
    ])

const createArticleList = defineStructure<DocumentListBuilder>((S, context) => {
  const user = context?.currentUser
  const roles = user?.roles.map((r) => r.name)
  const isLimited = roles?.includes('article-editor')

  let userQuery = ``
  if (isLimited) {
    userQuery = `createdBy == $userId`
  } else {
    userQuery = ``
  }

  return S.documentTypeList('article')
    .title(`Articles`)
    .filter([`_type == "article"`, userQuery].filter(Boolean).join(` && `))
    .params({userId: user?.id})
    .apiVersion(API_VERSION)
})

const createOffers = defineStructure<ListItemBuilder | undefined>((S, context) => {
  const roles = context?.currentUser?.roles.map((r) => r.name)
  const storesManaged = roles?.filter((r) => r.endsWith('-manager')).length

  if ((storesManaged && storesManaged > 1) || roles?.includes('administrator')) {
    return S.listItem()
      .title('Offers')
      .icon(TagIcon)
      .child(
        S.list()
          .title('Offers')
          .items(createStoreOffers(S, context) as ListItemBuilder[]),
      )
  } else if (storesManaged && storesManaged == 1) {
    return createStoreOffers(S, context) as ListItemBuilder
  }
})

const createStoreOffers = defineStructure<ListItemBuilder | ListItemBuilder[]>((S, context) => {
  const roles = context?.currentUser?.roles.map((r) => r.name)

  const userStores =
    stores
      .map((store) => {
        if (roles?.includes(`${store.id}-manager`) || roles?.includes('administrator')) {
          return S.listItem()
            .title(`${store.name} Offers`)
            .icon(HomeIcon)
            .child(
              S.documentTypeList('offer')
                .title(`${store.name} Offers`)
                .filter(`_type == "offer" && store == $storeId`)
                .params({storeId: store.id})
                .apiVersion(API_VERSION)
            )
        }
      })
      .filter((item) => !!item) || []

  return userStores?.length == 1 ? userStores[0] : userStores
})
```

This adds two new functions - `createOffers()` and `createStoreOffers()`. The `createOffers` function essentially checks whether the user has access to multiple stores or a single store. If they manage multiple, then a `listItem` is returned to add a top level item. In the case of single store management, then the `createStoreOffers()` function is returned.



At the top level, the returned values are spread into an array and a filter applied to remove empty items:



```typescript:src/structure/index.tsx
...[createOffers(S, context) as ListItemBuilder].filter(Boolean)
```

This allows us to return `undefined` from `createOffers` where the user does not have access to a store - as list items can’t accept undefined, this ensures compatibility with the expected types.



The `createStoreOffers()` function handles the output of the list item for each store, and includes a child `documentTypeList` which includes a filter to filter offers based on store ID.



> [!NOTE]
> Here we’ve customized just one section of our Structure, but you could create a completely unique Structures for different user roles in your business. For example, legal teams could just see legal content and merchandising teams could just see product information.



## Creating new documents



With this type of setup, it’s important to ensure that users can create new documents that they have relevant permissions for. Right now, even though a user profile may have “Store 1 Manager” and “Store 2 Manager” permissions, they won’t be able to create a new document:



![User can't create a new document, even though they should be able to](https://cdn.sanity.io/images/3do82whm/next/ec3918e88600980578baba1c0631f0e1641b26b7-2528x1660.png)

The reason for this is that when a new offer document is created, the “store” field will be empty – and these users only have permission to make edits when this field matches their store.



### Initial Value Templates



To solve this, initial value templates can be implemented in the Structure. These allow parameters to be passed based on where the user is in the Structure, so users can create offers relevant to the store list they’re viewing.



- [ ] **Update** your `sanity.config.ts` to define a new [parameterized initial value template](https://www.sanity.io/docs/initial-value-templates#66d873e2136f)


```typescript:sanity.config.ts
export default defineConfig({
  // rest of config
  schema: {
    types: schemaTypes,
    templates: (prev, context) => {
      const {currentUser} = context

      return [
        ...prev,
        {
          id: 'offer-by-store',
          title: 'Offer by store',
          description: 'Offer from a specific store',
          schemaType: 'offer',
          parameters: [
            {name: 'store', type: 'string'},
            {name: 'createdBy', type: 'string'},
          ],
          value: (params: {store: string}) => ({
            store: params.store,
            createdBy: currentUser?.id,
          }),
        },
      ]
    },
  },
})
```

Note that context can be used here - so contextual values based on the user could be populated - like user ID to a `createdBy` field. The key here though is that a `store` parameter is defined which can be passed from our Structure.



- [ ] **Update** the `createStoreOffers()` function in the Structure with the below code to set an initial value template for the store list


```typescript:src/structure/index.tsx
const createStoreOffers = defineStructure<ListItemBuilder | ListItemBuilder[]>((S, context) => {
  const roles = context?.currentUser?.roles.map((r) => r.name)

  const userStores =
    stores
      .map((store) => {
        if (roles?.includes(`${store.id}-manager`) || roles?.includes('administrator')) {
          return S.listItem()
            .title(`${store.name} Offers`)
            .icon(HomeIcon)
            .child(
              S.documentTypeList('offer')
                .title(`${store.name} Offers`)
                .filter(`_type == "offer" && store == $storeId`)
                .params({storeId: store.id})
                .apiVersion(API_VERSION)
                .initialValueTemplates([
                  S.initialValueTemplateItem('offer-by-store', {store: store.id}),
                ]),
            )
        }
      })
      .filter((item) => !!item) || []

  return userStores?.length == 1 ? userStores[0] : userStores
})
```

Now that this is setup, users can create new offers based on the context of the store they’re looking at, and the `store` field will be populated automatically:



![A document populated with initial values based on the structure](https://cdn.sanity.io/images/3do82whm/next/1a32e644049d5d04c6ed7b37610b5807db8bdf22-2528x1660.png)

This concept can be very useful outside of the context of role-based customizations, too!



### New Document Options



In addition to adding new documents via the Structure, users might also look to the “Create +” button in the Studio navigation bar.



![The "new document options" menu](https://cdn.sanity.io/images/3do82whm/next/6ad60d34517b7b9e979af904c71755398a990493-776x442.png)

Similarly to the Structure, the default options here may be disabled due to the permissions and initial values set up, again meaning a user can’t create documents with blank fields.



- [ ] **Update** your `sanity.config.ts` with the below code to change the available new document options


```typescript:sanity.config.ts
export default defineConfig({
  // rest of config
  document: {
    newDocumentOptions: (prev, {currentUser}) => {
      let removeTypes = ['media.tag', 'offer']

      const storeTemplates = stores.map((store) => {
        if (
          userHasRole(currentUser, `${store.id}-manager`) ||
          userHasRole(currentUser, 'administrator')
        ) {
          return {
            id: `${store.id}-offer`,
            templateId: 'offer-by-store',
            title: `${store.name} Offer`,
            parameters: {
              store: store.id,
            },
            type: 'template',
          }
        }
      }) as TemplateItem[]

      if (
        !userHasRole(currentUser, 'administrator') &&
        !userHasRole(currentUser, 'article-editor')
      ) {
        removeTypes.push('article')
      }

      return [...prev, ...storeTemplates.filter(Boolean)].filter(
        (templateItem) => !removeTypes.includes(templateItem.templateId),
      )
    },
  },
})
```

This customizes the available options when creating new documents to hide some options based on the user role – for example, removing the article type for users who aren’t admins or article editors and hiding metadata documents created by `sanity-plugin-media` .



Additionally, we’re using this menu to add the parameterized initial value templates too. This allows users to create documents for their own stores from the global menu.



## Custom components



There are some occasions that you might want to create custom components that are conditional based on user or role. This might include form components such as custom input components or could include other components like customizing the Studio layout, navbar or tool menu.



- [ ] **Create** a `StoreInput` component and add the below code to it


```typescript:src/components/StoreInput.tsx
import {Button, Grid, Text} from '@sanity/ui'
import {useCallback} from 'react'
import {set, type StringInputProps, type TitledListValue, useCurrentUser, userHasRole} from 'sanity'

export default function StoreInput(props: StringInputProps) {
  const {value, onChange, schemaType} = props

  const user = useCurrentUser()
  const roles = user?.roles.flatMap((r) => r.name)

  const handleClick = useCallback(
    (event: React.MouseEvent<HTMLButtonElement>) => {
      const nextValue = event.currentTarget.value
      onChange(set(nextValue))
    },
    [onChange],
  )

  const stores = (schemaType?.options?.list as Array<TitledListValue<'string'>>)?.filter(
    (option) => {
      return roles?.includes(`${option.value}-manager`) || userHasRole(user, 'administrator')
    },
  )

  return (
    <Grid columns={stores.length} gap={3}>
      {stores?.map((store) => (
        <Button
          key={store.value}
          value={store.value}
          mode={value === store.value ? `default` : `ghost`}
          tone={value === store.value ? `primary` : `default`}
          onClick={handleClick}
        >
          <Text size={1}>{store.title}</Text>
        </Button>
      ))}
    </Grid>
  )
}
```

- [ ] Add the custom input component to the `store` field of your offer document


```typescript:src/schemaTypes/offer.ts
defineField({
  name: 'store',
  title: 'Store',
  type: 'string',
  options: {
    list: stores.map((store) => {
      return {
        value: store.id,
        title: store.name,
      }
    }),
  },
  components: {
    input: StoreInput,
  },
}),
```

This input component demonstrates a few ideas:



1. Replacing the default radio input with buttons.

2. When the user is an admin, ensure the default options are rendered.

3. When the user is not an admin, adjust the available buttons to only show stores the user has access to, rather than the full list.


The screenshot below illustrates the Studio with no custom input alongside the views of an admin and a user with a limited number of stores. The custom component simplifies the user interface based on the users' role(s).



![Studios with and without the custom input component](https://cdn.sanity.io/images/3do82whm/next/3de0315606c260b61b6e529086b18e6f33401b4b-3530x1738.png)

This is a simple example to illustrate the point – you could implement similar principles to:



- Provide additional instructions alongside a field for users of a certain role.

- Change how a third party API is called in an input component based on user role.

- Amend the UI for content input based on whether a user is a developer or marketer.


## Conditional plugin configuration



Unfortunately, it’s not currently possible to customize plugin initialization based on role, as there is no user context here.



However, it is possible to selective (de)compose elements of a plugin in order to remove elements of a plugin based on user role. For example, the custom tool in `sanity-plugin-media` could be removed for some users by adding a custom plugin *after* the media plugin:



```typescript:sanity.config.ts
export default defineConfig({
  // rest of config
  plugins: [
    // other plugins
    media(),
    {
      name: 'disable-media-tool',
      tools: (prev, {currentUser}) =>
        userHasRole(currentUser, 'article-editor')
          ? prev.filter((tool) => tool.name !== 'media')
          : prev,
    },
  ],
})
```

## Workspaces per role



The Sanity Studio can have multiple workspaces - and each of these can have it’s own configuration. It’s a great idea to enable workspaces *per role…* for example, if I’m in a certain team, I want to work on certain content, in a particular workspace.



This *is* possible, but there is a caveat: when the Studio is initialized, the Studio configuration is handled before the user is authenticated - this means we don't know who the user is until after the workspaces are set up.



Because of this, you need to *wrap your Studio* to embed it in another React application. This allows you to make an API call to the user endpoint prior to initialization and provide different configuration based on the result.



Alternatively, you can [amend the Vite configuration](https://www.sanity.io/docs/development#9c7158c423fb) to allow for top-level async – but this can impact on some CLI commands such as GraphQL deployments, Typegen and document validation.



If this is something you would like to achieve, please speak to your Solution Architect.



---

## Lesson 6: Roles quiz
https://www.sanity.io/learn/course/introduction-to-users-and-roles/roles-quiz

A short test of everything you've learned through this course.

> **Question:** Custom roles are great for
>
> 1. Customizing the user experience of the Studio
> 2. Ensuring security, compliance and content integrity
> 3. Content workflows
> 4. Localization and market-specific content
> 5. All of the above **[correct]**

> **Question:** If a user is assigned the default "editor" role and a custom role which can only edit "article" documents, what can that user edit?
>
> 1. Nothing, because the roles conflict
> 2. Only the "article" documents
> 3. All documents **[correct]**

> **Question:** If I need to restrict the ability of a role to view certain documents, how should I configure my dataset?
>
> 1. It should be public
> 2. It should be private **[correct]**

> **Question:** What are content resources?
>
> 1. A set of documents in a dataset defined by a GROQ filter **[correct]**
> 2. Custom roles created for specific users
> 3. An API endpoint for selecting a group of documents
> 4. Schemas that define document structures

> **Question:** What does SAML role mapping allow you to do?
>
> 1. Control Studio access for groups of users
> 2. Set conditional access rules for specific content
> 3. Assign roles to users based on roles from a third-party identity provider **[correct]**
> 4. Navigate between two users' geographical locations

> **Question:** Which functions in Sanity Studio can you use to check the current user and validate their role?
>
> 1. getUser() and hasPermission()
> 2. useCurrentUser() and userHasRole() **[correct]**
> 3. fetchUser() and roleChecker()
> 4. whoAmI() and canIHazRole()

> **Question:** What value do end users get from studio customizations based on their role(s)?
>
> 1. A tailored editing experience that aligns with their responsibilities **[correct]**
> 2. Access to all hidden fields and content, regardless of their role
> 3. Dynamic Studio themes that match their favorite colors and personal tastes
> 4. An additional eight week vacation because of the efficiencies they enjoy 

---

## Related Resources

- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
