# Per-dataset TypeGen (beta) and clearer project access errors

**Version:** v3.9.0

**Published:** October 13, 2026

SDK hooks can now infer document, field, query, and projection types for each dataset your app reads, from types generated with `typegen.resources` in `@sanity/cli` 8.16.0. This release also explains the error users see when they sign in with an account that has no access to a project, and returns `projectId` and `dataset` as required from `createDocumentHandle`. Most apps need no code changes. Apps that store a created handle in state, a variable, or a context with an inferred type may need a type annotation, described below.

## Types for each dataset with TypeGen (beta)

With `@sanity/cli` 8.16.0 or later, list each dataset your app reads under `typegen.resources` in `sanity.cli.ts`, then run `sanity typegen generate`. TypeGen writes one types file per dataset and registers its types under that dataset's project ID and dataset. Hooks such as `useDocument`, `useQuery`, and `useDocumentProjection` look up types by the project ID and dataset on the handle, so the same document type or query can have different types in different datasets. `useDocument` with a `path` returns that field's type for the dataset.

**sanity.cli.ts**

```ts
import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
  typegen: {
    resources: [
      {
        projectId: 'abc123',
        dataset: 'production',
        schema: './schema.production.json',
        generates: './src/sanity.types.production.ts',
      },
    ],
  },
})
```

**BookCard.tsx**

```tsx
import {
  createDocumentHandle,
  defineProjection,
  useDocument,
  useDocumentProjection,
} from '@sanity/sdk-react'

const book = createDocumentHandle({
  projectId: 'abc123',
  dataset: 'production',
  documentId: 'book-1',
  documentType: 'book',
})

const authorProjection = defineProjection('book', '{"authorName": author->name}')

export function BookCard() {
  // string | undefined: the type of `title` in production
  const {data: title} = useDocument({...book, path: 'title'})
  // {authorName: string | null}
  const {data} = useDocumentProjection({...book, projection: authorProjection})
  return (
    <p>
      {title} by {data.authorName ?? 'Unknown'}
    </p>
  )
}
```

`defineProjection` now takes the document type as an optional first argument, as in `defineProjection('book', '{title}')`. TypeGen then generates the projection's type for that document type only, instead of for every document type in the schema. The function still returns the projection string unchanged.

When TypeScript only knows a handle's dataset or document type as `string`, such as a prop typed `DocumentHandle<'book'>` or a handle from `useDocuments` without a literal `documentType`, hooks return the union of the matching types across your generated datasets and document types.

This workflow is in beta. `typegen.resources`, the SDK's type resolvers, and `defineProjection` with a document type can change in minor releases. `sanity typegen generate --watch` and generation during `sanity dev` don't support `typegen.resources` yet. Types saved from the experimental TypeGen fork keep working for datasets without a new registration. The [TypeScript with TypeGen guide](https://reference.sanity.io/_sanity/sdk-react/TypeScript_with_TypeGen_%28beta%29/) covers setup, which dataset's types a hook uses, and migrating from experimental TypeGen.

## Clearer errors when a user signs in with the wrong account

Sanity creates a separate account for each sign-in method, even for the same email address. When a user signs in with an account that isn't a member of the project, the API rejects the request with a 401 `projectUserNotFoundError`. `useQuery`, `useDocuments`, and `usePaginatedDocuments` now throw this error with a message that names the sign-in method, such as Google or SSO, and tells the user to sign in with the account that has access. Error boundaries that render `error.message` show this message instead of `project user not found for user ID ...`. The error is still a `ClientError` with the original status code and response body.

When no error boundary in your app catches the error, the SDK's error screen shows the signed-in account, explains that each sign-in method is a separate account, and lists IDs for support. In standalone apps that don't use a static token, it also offers to sign out and switch accounts.

Call `getProjectAccessErrorProjectId(error)` in your error boundary to read the ID of the project that rejected the request. Rethrow the error when your app can't work without that project, so the SDK's screen replaces the app. Handle it in place when only part of your app reads from that project:

**Fallback.tsx**

```tsx
import {getProjectAccessErrorProjectId} from '@sanity/sdk-react'
import {type FallbackProps} from 'react-error-boundary'

const MAIN_PROJECT_ID = 'abc123'

export function Fallback({error}: FallbackProps) {
  const projectId = getProjectAccessErrorProjectId(error)
  // The app can't work without its main project, so show the SDK's screen instead
  if (projectId === MAIN_PROJECT_ID) throw error
  if (projectId) return <p>You don't have access to project {projectId}.</p>
  return <p>Something went wrong.</p>
}
```

`useDocument` and the projection hooks don't use the new message yet. `useDocument` reports a failed connection for this case, and projection hooks log fetch errors instead of throwing them.

## Handle creators return the project ID and dataset as required

`createDocumentHandle`, `createDocumentTypeHandle`, and `createDatasetHandle` in `@sanity/sdk` now return `projectId` and `dataset` as required when you pass both as strings. `createDocumentHandle({projectId: 'abc123', dataset: 'production', ...}).projectId` is typed `'abc123'` instead of `'abc123' | undefined`. Handles that omit either field return the same type as before.

A `useState` value, variable, or React context whose type is inferred from one of these handles now requires both fields. It no longer accepts a handle whose `projectId` and `dataset` are optional, such as a `DocumentHandle` prop or an item from `useDocuments`, and you can't set either field to `undefined`. Annotate the type with the document type, dataset, and project ID:

**useSelectedAuthor.tsx**

```tsx
import {createDocumentHandle, type DocumentHandle} from '@sanity/sdk-react'
import {useState} from 'react'

const firstAuthor = createDocumentHandle({
  projectId: 'abc123',
  dataset: 'production',
  documentId: 'author-1',
  documentType: 'author',
})

export function useSelectedAuthor() {
  const [author, setAuthor] =
    useState<DocumentHandle<'author', 'production', 'abc123'>>(firstAuthor)
  return {author, setAuthor}
}
```

## 🐛 Notable bugfixes and improvements

- In the Dashboard, apps configured with `resources` instead of `projectId` now show the access request prompt for the project that rejected a request, instead of an authentication error.
- `useNavigate` from `@sanity/sdk-react/dashboard` no longer requires a callback, so an app without its own router can call `useNavigate()`; an app with a router still passes one so the router follows the Dashboard's back and forward navigation.
- Editor hovers and type errors for document hooks show a TypeGen document type by its own name, such as `Movie`, instead of `DocumentRecord<Movie>`.

