Upload, browse and reference files stored outside of Sanity Studio.
Useful for large videos, audio files, archives, PDFs or any file you want to keep in R2, S3 or another storage backend.
- Remote files tool: Browse, search, upload and manage your remote file library.
remoteFilefield type: Upload or select remote files directly from document fields.- Playable previews: Play audio and video straight from the field, without opening the details dialog.
- File details dialog: Preview files, edit internal titles, inspect metadata and see which documents use a file.
- Cloudflare R2 provider: Worker-backed upload/delete flow with an interactive setup command.
- S3-compatible provider: Express API template for S3 or S3-compatible storage.
- Flexible providers: Use the bundled R2/S3 providers or add your own.
- Backend-only secrets: Provider credentials stay in your Worker/API, not in Sanity documents.
# npm
npm i sanity-plugin-remote-files
# yarn
yarn add sanity-plugin-remote-files
# pnpm
pnpm add sanity-plugin-remote-files
# bun
bun add sanity-plugin-remote-filesAdd it as a plugin in sanity.config.ts (or .js):
import {defineConfig} from 'sanity'
import {cloudflareR2Provider, remoteFiles} from 'sanity-plugin-remote-files'
export default defineConfig({
// ...
plugins: [
remoteFiles({
// configure title and description for specific needs like hosting videos only
// tool: {
// title: 'Videos',
// description: 'Browser, upload and manage videos.',
// },
providers: [
cloudflareR2Provider({
id: 'r2', // unique id allowing to setup multiple providers
endpoint: process.env.SANITY_STUDIO_R2_ENDPOINT,
publicUrl: process.env.SANITY_STUDIO_R2_PUBLIC_URL,
uploadPrefix: '', // 'uploads', 'videos', 'assets'…
headers: {
authorization: `Bearer ${process.env.SANITY_STUDIO_R2_TOKEN}`,
},
}),
],
}),
],
})Then use the remoteFile field type in your schema, in this example for videos:
import {defineField, defineType} from 'sanity'
export const page = defineType({
name: 'page',
type: 'document',
fields: [
defineField({
name: 'video',
title: 'Video',
type: 'remoteFile',
options: {
// accept: 'video/*', // limit to videos only
// requirePoster: true, // require a poster when the selected file is a video
provider: 'r2', // specify the provider id to use
},
}),
],
})Run the interactive R2 setup after installing the plugin:
npx sanity-plugin-remote-files setup r2 remote-files-r2-workerThe setup will:
- Create or reuse an R2 bucket
- Generate and deploy the Worker used by the plugin
- Configure the public file URL and allowed Studio origins
- Print the Sanity config snippet to add to your Studio
If you only want the Worker files without running Cloudflare setup:
npx sanity-plugin-remote-files setup r2 remote-files-r2-worker --template-onlyS3 setup scaffolds a small Express API template. Bucket, IAM and deployment differ between AWS setups, so those stay explicit.
npx sanity-plugin-remote-files setup s3 remote-files-s3-apiBoth templates refuse to start handling requests without REMOTE_FILES_SECRET: without it, anyone could upload to or wipe your bucket. The R2 setup command generates and deploys one for you.
| Variable | Default | Purpose |
|---|---|---|
REMOTE_FILES_SECRET |
— | Required bearer token for POST /upload and DELETE /files/:key. |
ALLOWED_ORIGINS |
— | Comma-separated Studio origins allowed by CORS. |
MAX_UPLOAD_MB |
100 |
Rejects larger uploads with 413. |
ALLOWED_CONTENT_TYPES |
all | Comma-separated video/mp4 or video/* entries. |
The token is passed from the Studio as an authorization header, so it is part of the Studio bundle and readable by anyone who can load the Studio. That is fine for a Studio behind a login you control. For a public Studio, use signedUrlProvider instead: your backend keeps the credentials and hands out short-lived upload URLs.
Main plugin function. Adds the remoteFiles.file document type, the remoteFile field type and the Studio tool.
providers: RemoteFilesProvider[]- configured storage providerstool.name?: string- tool route name. Default:'remote-files'tool.title?: string- tool title. Default:'Remote files'tool.description?: string- text shown at the top of the tool
accept?: string- value passed to the native file input, for example'video/*'or'.pdf'.provider?: string- provider id to use for this field. If omitted, the first provider is used.requirePoster?: boolean- require a poster when this field references a video file. Default:false.
Provider ids are stored on file documents and make multiple instances possible:
remoteFiles({
providers: [
cloudflareR2Provider({
id: 'marketing-r2',
endpoint: process.env.SANITY_STUDIO_MARKETING_R2_ENDPOINT,
}),
cloudflareR2Provider({
id: 'product-r2',
endpoint: process.env.SANITY_STUDIO_PRODUCT_R2_ENDPOINT,
}),
],
})Providers are not tied to Cloudflare Workers. The bundled R2 setup uses a Worker because it is a good fit for R2, but other providers can use any backend, SDK, signed URL flow or REST API.
Cloudflare R2 and the S3 template both use the same basic idea: the Studio talks to your Worker/API and that backend talks to storage with the real credentials.
The default backend contract is intentionally small:
POST /upload multipart/form-data with a `file` field
DELETE /files/:key delete a stored objectPOST /upload returns the file metadata the plugin stores in Sanity:
{
key: string
url: string
filename: string
contentType?: string
size?: number
duration?: number
width?: number
height?: number
}If another storage provider can follow that contract, adding it is usually just a small provider factory:
import {createRemoteFilesProvider} from 'sanity-plugin-remote-files'
export function myProvider(config: {id: string; endpoint: string; title?: string}) {
return createRemoteFilesProvider(config, {title: 'My Provider'})
}For signed URL flows, use signedUrlProvider with a backend endpoint that creates signed upload URLs and a delete endpoint. For anything else, providers can define custom uploadFile and deleteFile handlers. Keep secrets out of Studio code; prefer calling your own backend for privileged operations.
Files are stored as remoteFiles.file documents. A remoteFile field stores a reference to one of those documents.
*[_type == 'page']{
title,
video{
asset->{
_id,
title,
filename,
url,
key,
provider,
contentType,
duration,
width,
height,
size,
uploadedAt
}
}
}
For a cleaner frontend projection:
*[_type == 'page']{
title,
"video": video.asset->{
"title": coalesce(title, filename),
filename,
url,
contentType,
duration,
width,
height,
size
}
}
MIT © Félix Péault (Flayks)
This plugin uses @sanity/plugin-kit with default configuration for build & watch scripts.
See Testing a plugin in Sanity Studio on how to run this plugin with hotreload in the studio.