> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Configure your Media Library with an SDK config app

All the information needed to start configuring the Dashboard 2.0 version of Media Library with a config app.

> [!WARNING]
> Beta API
> The Dashboard extension API is in beta. Functions prefixed with `unstable_` can change without notice.

Aspects are the custom fields you add to assets in Media Library, such as descriptions, credits, SEO metadata, or locations. Previously, aspect definitions were limited to basic field types, without support for validation, initial values, or custom components.

You can now define aspects in code using a **config app**: a small app without a user interface, whose sole purpose is to define your Media Library's aspects. Each aspect is defined using `defineField`, the same API and feature set used for Studio schemas. You then deploy the app using the Sanity CLI.

Once deployed, the config app becomes the source of truth for your Media Library's custom fields across the Sanity Dashboard 2.0 ecosystem.

## Prerequisites

- A Sanity organization with Media Library enabled, and membership of that organization.
- The `sanity/cli@8.23.0` or above
- Understanding the basics of how [applications work in Dashboard 2.0](https://www.sanity.io/docs/dashboard-beta).

## Create a config app

By using the `init` command in the CLI with the `media-library-config` template we will bootstrap everything you need to get started.

```sh
sanity init --template media-library-config
```

> [!TIP]
> Create a restore point with Git or a backup
> Once you've initialized your config app and added your existing aspect definitions as code, it's a good idea to make an initial Git commit or create a backup. This gives you a starting point you can easily return to if needed.

If you previously have defined and deployed aspects for your Media Library, you will be asked if you want to use these as a starting point for your configuration app. We will turn them into the required code needed to have parity with your current Media Library’s aspects. You can then develop, preview and finally deploy the new fields to the new Dashboard version of the Media Library.

> [!NOTE]
> Backwards compatibility
> When you configure and deploy a Media Library config app, your existing Media Library (in the current Dashboard) will continue to work as before, unless you explicitly change the underlying field definitions (such as names or types).
> In Dashboard 2.0, Media Library will continue to use your existing aspect definitions until you deploy a config app.

## What a configuration looks like

**sanity.cli.ts**

```typescript
import {defineCliConfig, unstable_defineMediaLibrary} from 'sanity/cli'

export default defineCliConfig({
  app: unstable_defineMediaLibrary({
    organizationId: '<your-organization-id>',
    fields: [
      {name: 'description', title: 'Description', src: './src/fields/description.tsx'},
      {name: 'seo', title: 'SEO', src: './src/fields/seo.tsx', public: true},
    ],
  }),
})
```

Each entry in `fields` becomes one aspect:

- `src` points at a module whose default export is a `defineField(...)` definition.
- `name` must be the same as the `name` inside that `defineField(...)`. Asset values are stored under that name, so a mismatch leaves the aspect unable to find its data.
- The title people see in Media Library comes from the `defineField(...)` definition, not from the entry's `title`.
- `public: true` marks the aspect as public, the same way as before.

Every aspect needs a unique name. If two fields share a name, you will get a error message when running `sanity dev` or `sanity deploy`.

## Define fields

Field modules are [ordinary Studio field definitions](https://reference.sanity.io/sanity/index/defineField). Your fields configuration runs live in the Dashboard 2.0 version of the Media Library, because Media Library loads your field modules rather than a copy of them.

### Previewing your changes

While you develop on your fields, you can live preview them in your Dashboard 2.0 version of the Media Library by running the config app locally. You do this by running `sanity dev` from your configuration app root.

```sh
sanity dev
```

### Validation

**src/fields/description.tsx**

```typescript
import {defineField} from 'sanity'

export default defineField({
  name: 'description',
  title: 'Description',
  type: 'text',
  validation: (rule) => rule.required().max(500),
})
```

Validation messages appear next to the aspect's title in the asset sidebar.

#### Validation that depends on the asset

`context.document` is the whole asset document, so a rule can depend on the asset's other properties:

**src/fields/description.tsx**

```typescript
validation: (rule) =>
  rule.custom((value, context) => {
    const isPublicAsset = context.document?.cdnAccessPolicy === 'public'
    return isPublicAsset && !value?.holder ? 'Public assets need a copyright holder' : true
  }),
```

### Default values

An aspect's `initialValue` is applied once, right after an asset is uploaded. It never overwrites a value someone has already entered. It can be a value or a function, including an async one:

**src/fields/copyright.tsx**

```typescript
initialValue: () => ({year: new Date().getFullYear()}),
```

Only the aspect's top-level `initialValue` is applied. Put defaults on the aspect itself, not on its nested fields.

#### Defaults from the uploaded file

`initialValue` functions receive the uploaded asset and its file metadata as their second argument. For example, to pre-fill a location from a photo's EXIF GPS data:

**src/fields/geoLocation.tsx**

```typescript
initialValue: (_params, context) => {
  const location = context?.instance?.metadata?.location
  return location ? {lat: location.lat, lng: location.lng} : undefined
},
```

`defineField` TypeScript types don't declare this second argument yet, so in TypeScript you'll need to type `context` yourself and cast the function.

If an `initialValue` function throws, that aspect is left empty; the upload and other aspects are unaffected.

### Conditional fields

`hidden` works the same way as in Studio. `document` is the whole asset document, so you can show an aspect for only one asset type:

**src/fields/geolocation.tsx**

```typescript
hidden: ({document}) => document?.assetType !== 'sanity.imageAsset',
```

`assetType` is `sanity.imageAsset`, `sanity.videoAsset` or `sanity.fileAsset`.

### Custom input components

Write custom inputs exactly as you would for Studio:

**src/fields/notesInput.tsx**

```
import {set, unset} from 'sanity'
import {TextInput} from '@sanity/ui'

function NotesInput(props) {
  return (
    <TextInput
      value={props.value ?? ''}
      onChange={(event) => {
        const next = event.currentTarget.value
        props.onChange(next ? set(next) : unset())
      }}
    />
  )
}

export default defineField({
  name: 'notes',
  title: 'Notes',
  type: 'string',
  components: {input: NotesInput},
})
```

Each custom input runs on your config app's own React, `sanity` and `@sanity/ui` versions, isolated from Media Library. Media Library updates never require you to rebuild or redeploy.

Inside a custom input you can use these patch functions and hooks from the `sanity` package: 

| Patch functions | **Hooks** |
| --- | --- |
| `set` | `useFormValue` |
| `unset` | `useGetFormValue` |
| `setIfMissing` | `useCurrentLocale` |
|  | `useDateTimeFormat` |

`@sanity/ui` components work and follow the Dashboard 2.0 theme. 

`renderDefault` works for simple field types.

These aren't available inside Media Library:

- Workspace hooks such as `useClient`, `useCurrentUser` and `useWorkspace`.
- `renderDefault` for objects and arrays.
- `PatchEvent`. Call `props.onChange` with `set`, `unset` or `setIfMissing` patches instead.

If a custom input fails to load or crashes, Media Library shows a "This field couldn't be loaded" message in its place, and the rest of the form keeps working.

> [!WARNING]
> Using window globals and similar in custom components
> You can bundle npm dependencies, such as a map library. Put components that use browser-only libraries in their own module and load it lazily from your field module, so the field module itself can be imported without a browser.

## Deploy your config

```sh
sanity deploy
```

Once deployed, the config app's fields replace your Media Library's aspects in the Dashboard 2.0 and current version of Media Library. 

Removing or drastically changing a previously available aspect fields from the config removes or changes it from both versions of the Media Library (current and Dashboard 2.0 version). You will be prompted to confirm ‘yes’ by the `deploy` command if you are removing a field that’s currently in use in the current version of the Media Library.

> [!TIP]
> Modifying or removing deployed aspects
> Values already stored on assets are preserved even if the corresponding aspect or field is removed or changed. If you add a removed field back, its previously stored values will reappear in the editor.
> It's a good idea to keep a backup of your previous definitions (for example, in Git history) so you can restore them if something unexpected happens.
> Remember that you can always preview your updated configuration in the Dashboard 2.0 Media Library by running `sanity dev`. This lets you test your changes without affecting the deployed configuration until you run `sanity deploy`.



