Discussion on syncing user data and customizing the Sanity studio based on user access.

14 replies
Last updated: Sep 13, 2022
Hi! I'm trying to create a document for all users like this: https://www.sanity.io/schemas/create-a-document-for-all-current-project-users-68b6f0db but I really want it to try and sync each time the studio loads. Any ideas on how to go about this?
I've tried doing this:
https://www.sanity.io/schemas/custom-default-desk-pane-35148d61 but it doesn't load with a custom desk structure. I've also tried making my desk structure into a React component but that doesn't work.
AI Update

I understand you want to automatically create user documents for all project members each time the Studio loads. Unfortunately, the links you referenced appear to be broken or moved, but I can help you solve this problem.

The challenge you're facing is that certain hooks (like the one that might create initial documents) don't work with custom desk structures. Here are a few approaches to solve this:

Solution 1: Use a Studio Plugin with Layout Component

Instead of relying on desk structure lifecycle, create a Studio plugin that runs initialization logic when the Studio mounts:

// plugins/userDocumentSync.js
import {definePlugin} from 'sanity'
import {useEffect} from 'react'

export const userDocumentSync = definePlugin({
  name: 'user-document-sync',
  studio: {
    components: {
      layout: (props) => {
        useEffect(() => {
          // Fetch project members and sync user documents
          syncUserDocuments()
        }, [])
        
        return props.renderDefault(props)
      }
    }
  }
})

async function syncUserDocuments() {
  const client = sanityClient.withConfig({apiVersion: '2024-01-01'})
  
  // Fetch project members using Management API
  const members = await fetch(
    `https://api.sanity.io/v2021-06-07/projects/${projectId}/members`,
    {headers: {Authorization: `Bearer ${token}`}}
  ).then(res => res.json())
  
  // Create/update documents for each member
  for (const member of members) {
    await client.createIfNotExists({
      _id: `user.${member.id}`,
      _type: 'user',
      name: member.displayName,
      email: member.email
    })
  }
}

Then add this plugin to your sanity.config.js:

import {userDocumentSync} from './plugins/userDocumentSync'

export default defineConfig({
  // ...
  plugins: [
    // ... other plugins
    userDocumentSync()
  ]
})

For a more robust solution that doesn't depend on Studio loading, use Sanity Functions with a scheduled trigger:

// sanity.blueprint.ts
import {defineBlueprint} from '@sanity/blueprint'

export default defineBlueprint({
  functions: [
    {
      name: 'sync-user-documents',
      runtime: 'node22',
      schedule: '0 * * * *', // Run hourly
      handler: async (event) => {
        // Fetch members from Management API
        // Sync to dataset
      }
    }
  ]
})

This approach is better because:

  • It runs independently of Studio
  • Works even when no one has the Studio open
  • More reliable and scalable
  • Can be triggered on schedules or via webhooks

Solution 3: Tool Menu Action

Add a manual sync action to the Studio's tool menu that users can trigger when needed:

// tools/syncUsers.js
import {definePlugin} from 'sanity'

export const syncUsersTool = definePlugin({
  name: 'sync-users-tool',
  tools: [
    {
      name: 'sync-users',
      title: 'Sync Users',
      component: SyncUsersComponent
    }
  ]
})

Key Points

  • Custom desk structures bypass some Studio lifecycle hooks, which is why your approach wasn't working
  • Sanity Functions are the modern, recommended approach for automation like this - they run independently of Studio and can be triggered on schedules or events
  • You'll need to use the Sanity Management API to fetch project members at https://api.sanity.io/v2021-06-07/projects/{projectId}/members
  • Consider using createIfNotExists() to avoid duplicates
  • The plugin layout component approach (Solution 1) will work for your specific "sync on Studio load" requirement, but Functions (Solution 2) provide a more reliable solution

The plugin with layout component wrapper is your best bet for the "run every time Studio loads" requirement with a custom desk structure. It works by wrapping the entire Studio layout rather than hooking into the desk structure specifically.

I think I found a way to do this, but it feels illegal:
Basically made the Desk structure async..


const DeskStructure = async () => {
  await syncUsers()
  return S.list()
}
Wait where do you want to get the user docs into? This seems indeed strange to do. Can you give me more input, so I can try understand what your goal is?
(this still makes me giggle btw: I think I found a way to do this, but it feels illegal)
Hi Saskia! I have a studio that runs 5 different frontend sites. We've organized it so that most documents (pages/articles etc) reference a "domain document" and fetch the correct frontend using multiple deployments in Vercel and environment variables to set the domain id.
Now we want to have a different view for the editors based on which page they can access. This is purely visual I know since no enterprise, but works well for us.

The jist is:
• Sync Sanity users to a document called 'sanityUser' on each studio load.
• The sanityUser document contains an array of references to domains, the "sanity id" of the user, as well as a "superadmin" boolean
• In the DeskStructure we then find the current users ID and find the corresponding document. We then filter the desk structure based on their access
If you are interested it looks like this:


// Desk structure
const DeskStructure = async () => {
  await syncUsers()
  const currentUser = await userStore.getCurrentUser()
  const { superAdmin, siteAccess } = await studioClient.fetch(
    `*[_type == 'sanityUser' && id == $id][0]{
      superAdmin,
      "siteAccess": siteAccess[]._ref
    }`,
    { id: currentUser.id }
  )

  if (!superAdmin) {
    return S.documentTypeList("domain")
      .filter("_id in $siteAccess")
      .params({ siteAccess })
      .title("Nettsider")
      .child((domainID) => domainDocumentList(domainID))
  }
  
  // Superadmin view
  return S.list() 
}

// Sync users
import client from "part:@sanity/base/client"
import cq from "concurrent-queue"
import userStore from "part:@sanity/base/user"
const studioClient = client.withConfig({ apiVersion: "2022-03-25" })

// Create a queue to limit the rate at which you write changes to Sanity
let queue = cq()
  .limit({ concurrency: 2 })
  .process(function (task) {
    return new Promise(function (resolve, reject) {
      setTimeout(resolve.bind(undefined, task), 1000)
    })
  })

const syncUsers = async () => {
  //query for all members, then use the userStore to get their details

  const [allUsers, existingUsers] = await Promise.all([
    studioClient.fetch(`*[_id in path('_.groups.*')].members[@ != 'everyone']`),
    studioClient.fetch(`*[_type == "sanityUser"].id`),
  ])

  // Filter to to the users that are not already existing
  const newUsers = allUsers
    .flat()
    .filter((user) => !existingUsers.includes(user))

  if (newUsers.length == 0) return

  // Fetch the user details from the userStore
  const userDetails = await userStore.getUsers(newUsers)

  for (const user of userDetails) {
    const doc = {
      _type: "sanityUser",
      _id: "sanityUser." + user.id,
      id: user.id,
      name: user.displayName,
    }
    queue(doc).then(async () => {
      //create the doc via the client
      studioClient
        .create(doc)
        .then((updatedDoc) => {
          console.log(
            `Hurray, the doc is updated! New document:${updatedDoc._id}`
          )
        })
        .catch((err) => {
          console.error("Oh no, the update failed: ", err.message)
        })
    })
  }
}

export default syncUsers
So it works haha. And I think this technique can be used to fully customize the studio based on document types etc 😄
I will def have a more detailed look on Monday and try out your approach 🫡If it's illegal we will both go to jail then
🚨 #partnersInCrime
I am just spitballing here, but maybe another approach (not sure if better or worse) would be to get some thing working in the stucture builder `hidden doc types`….

// /deskStructure.js

// Hide document types that we already have a structure definition for
const hiddenDocTypes = (listItem) =>
    ![
        "siteSettings",
        //some async functionality here

    ].includes(listItem.getId());

export default () =>
    S.list()
        .title('Content')
        .items([
            site1Docs,
            S.divider(),
            // your other desk partitions
            S.divider(),
            siteSettings,

            ...S.documentTypeListItems().filter(hiddenDocTypes)
        ])
This def works and is a solution we've used a lot, but it doesn't show a different view based on what user is logged in
i mean, that you could try the same kind of rendering which you have now, but use the userStore info … dont know if it works, but I am always for testing our performance…
And tbh, as long as it works, I would try stuff, thats the beauty of open source right?
Indeed, very happy with this discovery as this means we can basically make custom roles on the free plan
Everyone of course has the same access via the actual API etc but this will suit our projects nicely
yeah its more render access, but a workaround.

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.

Was this answer helpful?