Understanding pricing tiers and project structure in Sanity.io

60 replies
Last updated: Jun 16, 2023
Not sure if this is the right channel, but I have a pricing question... The "business" tier is $949 per project. What is a project? If our team manages 4 different apps, will that be $3796 or is our team considered 1 project?
May 31, 2023, 8:46 PM
A project is what’s created when you run
sanity init
. Basically the parent identifier for your datasets, project members, etc. contained therein.
You can have as many apps as you’d like connected to a single project, even within a single dataset in that project. The Content Lake is unopinionated about where you’re presenting your data.
May 31, 2023, 8:56 PM
I see, thanks! I feel like keeping everything in the same project might get a little messy though, it would be nice to have that separation. Do you happen to know if an enterprise plan includes X amount of projects, or at what point it might be cost beneficial?
May 31, 2023, 9:01 PM
An Enterprise plan would be for a single project, but you could get a custom number of datasets to help with that separation.
It’s also totally possible to create that separation in a single free project with a single dataset. You can use a combination of
paths and a custom structure to to keep things organized.
Enterprise becomes beneficial when you need features like custom Roles, Advanced Dataset Management, longer History retention, and the like. If it’s just for the sake of having more datasets you may be a bit unhappy with the cost of an Enterprise plan.
May 31, 2023, 9:06 PM
Oh, and I forgot: workspaces ! Those are super beneficial for managing different app as well.
May 31, 2023, 9:07 PM
ooo, lovely, I haven't come across these features
May 31, 2023, 9:07 PM
user M
is it possible to control user permissions on a workspace level? I.e. Can we limit users to one workspace but not another?
Jun 2, 2023, 3:46 PM
It is! Here’s one example we’ve used in the past:
import {Config, defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
import {visionTool} from '@sanity/vision'
import {schemaTypes} from './schemas'

const projectId = 'aaaaaahh'

// use top-level await to get the user.
const meResponse = await fetch(`https://${projectId}.<http://api.sanity.io/v2021-06-07/users/me|api.sanity.io/v2021-06-07/users/me>`,
 {credentials: "include"}

const me = await meResponse.json()

// If the user is logged in, we will have some values in `me`.
// If not, it will be an empty object
console.log("me", me)

// realConfig should be based on the role(s) that the user has (check me.roles)
// it could be an array of workspaces or just a single config
const realConfig: Config = {
  name: 'default',
  title: 'Actual Workspace',

  dataset: 'production',
  tools: [],

  plugins: [deskTool(), visionTool()],

  schema: {
    types: schemaTypes,

// Login config is used just to trigger the login flow
// the user should never actually get to this UI
// because we swap the configs when the login navigation redirects back to the Studio.
// However you could consider having it as a fallback environment
const loginConfig: Config = {
  name: 'default',
  title: 'login',
  dataset: 'v3-sandbox',
  tools: [],
  plugins: [],
  studio: {
    components: {
      // This makes the studio just render nothing
      // you could replace this with a React component if you want
      // but the user should never see this unless you want to use it as a fallback for when no roles match.
      // Normally this is used to wrap the default layout so you can provide a banner across the top of the screen
      // or a React context provider.
      layout: () =&gt; null

const config = me.roles ? realConfig : loginConfig

export default defineConfig(config)
Jun 2, 2023, 4:44 PM
Ah, I see so you can access the user's credentials and programmatically modify the studio based on that? Would that be a way to get around the 20 non-admin user limit? The Business plan is looking perfect for us, except for that we have about 30 blog contributors that shouldn't be admins.
Jun 2, 2023, 4:53 PM
You couldn’t really get around the non-admin user limit this way, since this is based on the user’s role in the system. You could hack your way around it, though, by obscuring documents using a custom structure and custom document actions, though. However, it wouldn’t be as secure as using the built in roles system. Documents would still be discoverable in the search bar and via the API.
Jun 2, 2023, 5:06 PM
I see... and with the business plan you don't get any custom roles right? So there wouldn't be a way to have a "project A" role and a "project B" role?
Jun 2, 2023, 5:08 PM
Correct, you’d need an Enterprise plan to get that sort of granularity in your roles. Without it, you could create totally different projects with different roles for a given member, but that would probably be a major hassle to maintain!
Jun 2, 2023, 5:13 PM
Right, and then your cost multiplies with the projects
Jun 2, 2023, 5:14 PM
Unless you can get away with some of them being on the free plan
Jun 2, 2023, 5:15 PM
Can you give a rough estimate where Enterprise costs start? In my experience there is a steep increase, but we would really only need it for the roles/non-admin users.
Jun 2, 2023, 5:16 PM
I like to think of it this way: since the Studio is just a React app, you can basically replicate most of the Business and Enterprise features on your own if you’re willing to throw the dev time to building/maintaining it and accept some brittle solutions. Sometimes that cost is just way too high so it ends up being more convenient to just pay for it outright. Sometimes you can just get away with rolling your own solution because it’s good enough!
I’m pretty far removed from the sales side of things, so I can’t really give you a good idea of a starting point for an Enterprise plan. There’s just too much variability for me to guess, unfortunately. But I know enough to know that you are correct that it’s a steep increase from the Business plan.
Jun 2, 2023, 5:21 PM
Ha, ok yes that's my assumption... ok well thank you very much!!! This has been very enlightening. I'm going to play with this and see what I can come up with.
Jun 2, 2023, 5:24 PM
For sure! Happy to help! Let me know if you have any other questions!
Jun 2, 2023, 5:24 PM
user M
are you familiar with the different between Business Tier SSO and Enterprise Tier SSO?
Jun 5, 2023, 9:21 PM
I think the difference is that you can create rules for automatically assigning roles in Projects in Enterprise, whereas you’d need to explicitly set the role in the manage dashboard on business. That said, I’m not an SSO expert, so it’s possible that that is not at all true.
Jun 5, 2023, 9:31 PM
user M
one more QQ 🙂
Using your tips, I think the Business plan will work... however, to ease the powers that be, IF down the road we decide we need Enterprise features, would that be an easy upgrade to our existing account, or is it starting from scratch?
Jun 6, 2023, 6:51 PM
Upgrading to Enterprise would just be a matter of us toggling features in on your existing project. No starting from scratch! Depending on the size of your org, though, the sales process can take a while to work through. Usually if you’re already using Sanity and your internal stakeholders have bought in it speeds up the process!
Jun 6, 2023, 6:54 PM
If I were to have a schema named
, and I want to change the name to
I should be able to modify the dataset with groq to update all existing `page`s and convert them to `companyPage`s correct? What's the best way to do that, node script? Is there a doc on that?
Jun 6, 2023, 8:11 PM
user M
sorry I know I'm abusing this thread 🙂 I'm taking machine gun questions from all angles so I appreciate your help!
Jun 6, 2023, 8:16 PM
nm, I think I found what I need https://www.sanity.io/docs/js-client
Jun 6, 2023, 8:23 PM
You can’t directly modify your data with GROQ, but you can use it to query for the documents you need to change, then use the JS client you discovered to patch them 🙂
Jun 6, 2023, 8:51 PM
Thanks for confirming!
Jun 6, 2023, 9:13 PM
Gah... I wrote out the script, but it's erroring with
immutable attribute \\"_type\\" may not be modified"
. Is there any way to update the _type?
Jun 6, 2023, 9:38 PM
Ah, you have to create a new document of that type, then delete the old one. I forgot, we actually have this snippet for you to use.
Jun 6, 2023, 9:40 PM
beautiful, thanks!
Jun 6, 2023, 9:41 PM
user M
would you mind looking at this for me? I'm banging my head against a wall. What I'm trying to change is the name of an object that is used as part of an array (i.e. a module within a page content section). I believe I have to dup the page with a new _id, update any references to the original _id to the new _id (in my case, it's referenced in an array of references within a nav document), then delete the old page. I thought I've done that, but it keeps erring "documentHasExistingReferencesError." The funny thing is if I take out the delete step, the duplicate and update nav steps are successfully created and patched, so I'm not sure why it can't delete the old page.
    // within iterating over each existing document
    mutations.push({create: newDocument})
      if (!doc.incomingReferences) {
        console.log('no refs')
      // Patch each of the incoming references
      doc.incomingReferences.forEach((referencingDocument) =&gt; {
        if (!referencingDocument.items?.length) {
          console.log('no items')
        const newItems = referencingDocument.items.map((item) =&gt;
          item._ref === doc._id ? {...item, _ref: newDocId} : item
          id: referencingDocument._id,
          patch: {
            // unset: ['items'],
            set: {...referencingDocument, items: newItems},
            ifRevisionID: referencingDocument._rev,
      mutations.push({delete: doc._id})
      const transaction = createTransaction(mutations)
      await transaction.commit()
Jun 7, 2023, 6:01 PM
You don't have to debug the whole thing 😂 but if there are any "gotchas" working with arrays that would be great to know 🙂
Jun 7, 2023, 6:03 PM
(Or if there's an easier way to update the _type of an object within a document without duplicating the page)
Jun 7, 2023, 6:04 PM
Oh yeah, you don’t need to create an entirely new document if you’re changing the type of an object inside of it. Can you share some example JSON for one of the documents containing this object?
Jun 7, 2023, 6:19 PM
  "_createdAt": "2023-04-26T11:50:13Z",
  "_id": "60f77d3c-ae7a-406b-94e2-dc887ffb5e2f",
  "_rev": "LFnfZxO6WaODD0jffUqNAG",
  "_type": "page",
  "content": [
      "_key": "ccd1c1cd39c2",
      "_type": "capstone",
      "backgroundImage": {
        "_type": "image",
        "asset": {
          "_ref": "image-f120fada92842b83b7a2a11aada8324f4ce7bb78-2070x1380-png",
          "_type": "reference"
      "capTitle": [
          "_key": "ee1d7effed9f",
          "_type": "block",
          "children": [
              "_key": "8df4b7543255",
              "_type": "span",
              "marks": [],
              "text": "Hi guys! This should update in real time. This is a "
              "_key": "b09150cc71e2",
              "_type": "span",
              "marks": [
              "text": "Capstone."
          "markDefs": [],
          "style": "normal"
      "imageVariant": "left",
      "variant": "text_left"
      "_key": "ad41e82cf18d",
      "_type": "cards",
      "description": "percent",
      "heading": "Test Heading"
      "_key": "a8da77e4ea73",
      "_type": "simpleBanner",
      "backgroundColor": "blue",
      "heading": "simple banner"
      "_key": "52d61f573b73",
      "_type": "infocard",
      "cta": {
        "label": "cta label",
        "link": "<https://epsilon.com/us>"
      "icon": "none",
      "paragraph": "This is the infocard text",
      "title": "InfoCard Title"
      "_key": "4e9933c262e3",
      "_type": "quote",
      "blurb": "This is the blurb",
      "cta": {
        "label": "CTA goes here",
        "link": "<https://epsilon.com/us>",
        "new_tab": true
      "icon": "old icon text",
      "image": {
        "_type": "image",
        "asset": {
          "_ref": "image-5a8e43276dd37ffd2b4e1e196e740e13dae2fe17-200x200-png",
          "_type": "reference"
      "paragraph": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
      "title": "quote section title"
  "name": "Off-site",
  "slug": {
    "_type": "slug",
    "current": "off-site"
  "_updatedAt": "2023-06-07T18:04:13.026Z"
Jun 7, 2023, 6:21 PM
Let’s use greetings like “Hey Everyone,” “Hi, Y’all," or “Hello, Folks” to make sure everyone in the community is included. Please read the Sanity Community Code of Conduct to stay updated on expected communication &amp; behavior in our spaces: https://www.sanity.io/docs/community-code-of-conduct
Jun 7, 2023, 6:21 PM
Say I want to change "capstone" to "hero"
Jun 7, 2023, 6:21 PM
So in this case, I’d set up my function to do the following:1. Find any elements with
_type: 'capstone'
in the
array2. Create a new object with
_type: 'hero'
that spreads the rest of the fields from the old capstone object into it3.
Delete the
objects from the array.4.
Set the new objects in the right place in the array.This will be easier because you won’t have to worry about any incoming references/updating them in other documents.
Jun 7, 2023, 6:29 PM
I can try to put together an example script later this afternoon if time allows!
Jun 7, 2023, 6:29 PM
No worries! I will work on that right now and if I can get that to work I'll let you know.
Jun 7, 2023, 6:30 PM
Jun 7, 2023, 6:30 PM
Let me know how I did. Seems to be working
Jun 8, 2023, 11:51 AM
user M
I also wanted to ask you more about paths... seems like paths are on the ID's, but I'm not clear on how we set the ID. I also see that ID's with .'s get ignored for unauthententicated users. If we have a document type that gets used across multiple workspaces, is there a way to automatically prepend a path to a document ID based on the current workspace? Or would it be better to have separate document names per workspace?
Jun 8, 2023, 3:02 PM
You can have a path that’s used across multiple workspaces. It would just be a matter of adding the paths you want to reveal to your Structure in a workspace and leaving out the rest.
Jun 8, 2023, 4:59 PM
But say I have a "page" document schema that's used across multiple workspaces. If a user creates a page while in Workspace A, is it possible to add a workspaceA path automatically?
Jun 8, 2023, 8:10 PM
user M
I had a pretty good solution going accessing the user data with
// use top-level await to get the user.
const meResponse = await fetch(`https://${projectId}.<http://api.sanity.io/v2021-06-07/users/me|api.sanity.io/v2021-06-07/users/me>`, {
  credentials: 'include',
but I just tried to build studio and got
ERROR: Top-level await is not available in the configured target environment
Jun 8, 2023, 10:59 PM
Any ideas?
Jun 8, 2023, 10:59 PM
user M
Been OOO for a few days but back now 🙂 Any ideas on the top-level await issue above?
Jun 13, 2023, 4:50 PM
Ah, there’s something in the vite config you have to change. Let me see if I can find it.
Jun 13, 2023, 5:15 PM
Jun 13, 2023, 5:16 PM
Awesome thank you! I'll test this out in a few minutes...
Jun 13, 2023, 5:16 PM
Let me know if it works!
Jun 13, 2023, 5:17 PM
user M
success! Thank you once again.
Jun 14, 2023, 3:12 PM
user M
When you get a minute, I still had a question about paths. Paths apply to the ID's of individual instances of content, correct? (i.e. a page that the content editor has published) I'm wondering, in the event that 2 different workspaces have a document schema with the same name (e.g. "page") is there some way we can also use paths to segment by workspace so on brandA.com we can pull pages with the brand-a. path, and on brandB.com we can pull pages with the brand-b. path. Or an I misunderstanding how paths work?
Jun 14, 2023, 3:18 PM
user M
No promises, but I think this ☝️is the last question before we kick off the build 🙂
I'm basically just trying to figure out how to organize the project so that there are safeguards against content cross-contamination between sites.

I appreciate all your help, and this thread itself has been a valuable selling point.
Jun 16, 2023, 4:47 PM
Yep! Definitely possible. It would be fairly simple:• Brand A would have an id like:
• Brand B would have an id like:
Then, in your structure, you could do something like:

        .child((id, context) =&gt; {
          const workspaceName = context.structureContext.name
          return S.documentList().title('Pages').schemaType('page').filter(`_id in path(${workspaceName}.**`)
This is a simplified version, since the workspace name wouldn’t directly match the path. You’d probably need more logic to match it with whatever string you use for your path, then use
that path.
Jun 16, 2023, 6:46 PM
user M
That explains how to display it, but how would you set that path when the page is created?
Jun 16, 2023, 7:17 PM
Initial values to set it, document actions to maintain it.
Jun 16, 2023, 7:17 PM
Is there documentation or example on how to do that? That's what I've been having a hard time finding
Jun 16, 2023, 7:22 PM
Like would it be an initial value set to
+ a SHA256 hash?
Jun 16, 2023, 7:23 PM
There’s documentation on those concepts, but not this particular use case. This is where Enterprise is nice because then you get Solution Engineers to work with to build it.
Jun 16, 2023, 7:23 PM

Sanity– build remarkable experiences at scale

The Sanity Composable Content Cloud is the headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?

Categorized in