Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 73 additions & 53 deletions skills/sanity-best-practices/references/app-sdk.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
---
title: Sanity App SDK
description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices.
description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices. Use these rules when users ask to 'Make an SDK app' or 'Make a sanity application'.
---

# Sanity App SDK

Build custom React applications that interact with Sanity content in real-time.

## Before Scaffolding

Before running any CLI command or writing any code:

1. **Organization** — call `list_organizations`. For each org returned, call
`get_current_user_resource_permissions` with `resourceType: "organization"`,
`resourceId: <orgId>`, and `permissions: ["sanity.sdk.applications.deploy"]`.
Only present orgs where that permission is granted. If none qualify, stop and
tell the user they don't have permission to deploy Sanity apps.

2. **Project** — call `list_projects` with `organizationId: <chosen-org-id>` and
`onlyExplicitMembership: true`. For the user's chosen project, call
`get_current_user_resource_permissions` with `resourceType: "project"`,
`resourceId: <projectId>`, and `permissions: ["sanity.project.read"]`.
If they have no projects or want a new one, offer to call `create_project`.

3. **Template** — ask: basic quickstart, or with Sanity UI?

Do not proceed until `organizationId` and `projectId` are confirmed.

## Tech Stack

- **Framework:** React 19+, TypeScript
- **Packages:** `@sanity/sdk`, `@sanity/sdk-react`
- **Optional UI:** `@sanity/ui`, `styled-components`
- **Packages:** `@sanity/sdk`, `@sanity/sdk-react` – make sure you always use the latest versions
- **Optional UI:** `@sanity/ui`, `styled-components` – make sure you always use the latest versions
- **Runtime:** Node.js 20+

## Commands
Expand Down Expand Up @@ -63,14 +83,14 @@ my-app/
### CLI Config (`sanity.cli.ts`)

```typescript
import { defineCliConfig } from 'sanity/cli'
import { defineCliConfig } from "sanity/cli";

export default defineCliConfig({
app: {
organizationId: 'your-org-id',
entry: './src/App.tsx',
organizationId: "your-org-id",
entry: "./src/App.tsx",
},
})
});
```

### App Root (`src/App.tsx`)
Expand Down Expand Up @@ -137,44 +157,44 @@ Lightweight references to documents. Fetch handles first, then load content as n

```typescript
interface DocumentHandle {
documentId: string
documentType: string
projectId?: string
dataset?: string
documentId: string;
documentType: string;
projectId?: string;
dataset?: string;
}
```

### Creating Handles

```typescript
// Best: From useDocuments hook
const { data: handles } = useDocuments({ documentType: 'article' })
const { data: handles } = useDocuments({ documentType: "article" });

// Good: With helper (preserves literal types for TypeGen)
import { createDocumentHandle } from '@sanity/sdk'
import { createDocumentHandle } from "@sanity/sdk";
const handle = createDocumentHandle({
documentId: 'my-doc-id',
documentType: 'article',
})
documentId: "my-doc-id",
documentType: "article",
});

// Good: With as const (preserves literal types)
const handle = {
documentId: 'my-doc-id',
documentType: 'article',
} as const
documentId: "my-doc-id",
documentType: "article",
} as const;
```

---

## Hook Selection

| Hook | Use Case | Returns |
|------|----------|---------|
| `useDocuments` | List of documents (infinite scroll) | Document handles |
| `usePaginatedDocuments` | Paginated lists with page controls | Document handles |
| `useDocument` | Single document, real-time editing | Full document or field |
| `useDocumentProjection` | Specific fields, display only | Projected data |
| `useQuery` | Complex GROQ queries (use sparingly) | Raw query results |
| Hook | Use Case | Returns |
| ----------------------- | ------------------------------------ | ---------------------- |
| `useDocuments` | List of documents (infinite scroll) | Document handles |
| `usePaginatedDocuments` | Paginated lists with page controls | Document handles |
| `useDocument` | Single document, real-time editing | Full document or field |
| `useDocumentProjection` | Specific fields, display only | Projected data |
| `useQuery` | Complex GROQ queries (use sparingly) | Raw query results |

---

Expand Down Expand Up @@ -348,8 +368,8 @@ function VenuesList() {
```typescript
// Bad: Multiple fetchers in one component
function BadComponent() {
const { data: events } = useDocuments({ documentType: 'event' })
const { data: venues } = useDocuments({ documentType: 'venue' })
const { data: events } = useDocuments({ documentType: "event" });
const { data: venues } = useDocuments({ documentType: "venue" });
// Both trigger Suspense together, causing unnecessary re-renders
}
```
Expand Down Expand Up @@ -379,27 +399,27 @@ function OpenInStudioButton({ handle }: { handle: DocumentHandle }) {
## Event Handling

```typescript
import { useDocumentEvent, DocumentEvent } from '@sanity/sdk-react'
import { useDocumentEvent, DocumentEvent } from "@sanity/sdk-react";

function DocumentWatcher(handle: DocumentHandle) {
useDocumentEvent({
...handle,
onEvent: (event) => {
switch (event.type) {
case 'edited':
console.log('Edited:', event.documentId)
break
case 'published':
console.log('Published:', event.documentId)
break
case 'deleted':
console.log('Deleted:', event.documentId)
break
case "edited":
console.log("Edited:", event.documentId);
break;
case "published":
console.log("Published:", event.documentId);
break;
case "deleted":
console.log("Deleted:", event.documentId);
break;
}
},
})
});

return null
return null;
}
```

Expand All @@ -409,17 +429,17 @@ function DocumentWatcher(handle: DocumentHandle) {

```typescript
const config: SanityConfig[] = [
{ projectId: 'project-1', dataset: 'production' },
{ projectId: 'project-2', dataset: 'staging' },
]
{ projectId: "project-1", dataset: "production" },
{ projectId: "project-2", dataset: "staging" },
];

// Handles include project/dataset info
const handle: DocumentHandle = {
documentId: 'doc-123',
documentType: 'article',
projectId: 'project-1',
dataset: 'production',
}
documentId: "doc-123",
documentType: "article",
projectId: "project-1",
dataset: "production",
};
```

---
Expand Down Expand Up @@ -455,8 +475,8 @@ The App SDK provides hooks and data stores. You bring:

## Troubleshooting

| Issue | Solution |
|-------|----------|
| Safari dev issues | Use Chrome or Firefox during development |
| Port 3333 in use | `npm run dev -- --port 3334` |
| Auth errors | `npx sanity@latest logout && npx sanity@latest login` |
| Issue | Solution |
| ----------------- | ----------------------------------------------------- |
| Safari dev issues | Use Chrome or Firefox during development |
| Port 3333 in use | `npm run dev -- --port 3334` |
| Auth errors | `npx sanity@latest logout && npx sanity@latest login` |