Dyrected
Adapters

Storage Adapters

Complete guide to configuring file storage, upload options, and image resizing.

Dyrected delegates file storage to a storage adapter. The adapter determines where uploaded files are stored and how their public URLs are resolved. You configure one adapter globally and it is used by all upload-enabled collections.

On Dyrected Cloud, storage is fully managed. No adapter config needed — files are stored in Dyrected's cloud storage with automatic CDN delivery. Learn more →


Local File System

Stores files on the server's local disk. Best for development and single-server deployments. Not suitable for serverless (Vercel, Cloudflare Workers) because the filesystem is ephemeral.

import { LocalStorageAdapter } from '@dyrected/storage-local'

export default defineConfig({
  storage: new LocalStorageAdapter({
    uploadDir: './public/uploads',   // Directory to write files to
    staticUrlPrefix: '/uploads',        // Public URL prefix
  }),
})

Files are served as static assets by your framework's public directory. The url on a media document will be /uploads/filename.jpg.


AWS S3 (and S3-Compatible Services)

Use for production and serverless environments. Compatible with AWS S3, DigitalOcean Spaces, Backblaze B2, MinIO, Cloudflare R2, and any other S3-compatible API.

import { S3StorageAdapter } from '@dyrected/storage-s3'

export default defineConfig({
  storage: new S3StorageAdapter({
    bucket: process.env.S3_BUCKET!,
    region: process.env.S3_REGION!,
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY!,
      secretAccessKey: process.env.S3_SECRET_KEY!,
    },
    // Optional: custom endpoint for non-AWS S3-compatible providers
    endpoint: process.env.S3_ENDPOINT,      // e.g. 'https://nyc3.digitaloceanspaces.com'
    forcePathStyle: false,                  // Set true for MinIO
    // Optional: custom domain or CDN URL prefix
    baseUrl: process.env.CDN_URL,            // e.g. 'https://cdn.mysite.com'
    // Optional: ACL for uploaded objects
    acl: 'public-read',                     // default: 'public-read'
  }),
})

DigitalOcean Spaces example

new S3StorageAdapter({
  bucket: 'my-space',
  region: 'nyc3',
  endpoint: 'https://nyc3.digitaloceanspaces.com',
  credentials: {
    accessKeyId: process.env.DO_SPACES_KEY!,
    secretAccessKey: process.env.DO_SPACES_SECRET!,
  },
  baseUrl: 'https://my-space.nyc3.cdn.digitaloceanspaces.com',
})

Cloudflare R2 example

new S3StorageAdapter({
  bucket: 'my-r2-bucket',
  region: 'auto',
  endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY!,
    secretAccessKey: process.env.R2_SECRET_KEY!,
  },
  baseUrl: process.env.R2_PUBLIC_DOMAIN,   // Your custom R2 public domain
})

Cloudinary

For advanced image optimisation, transformations, and CDN delivery.

import { CloudinaryStorageAdapter } from '@dyrected/storage-cloudinary'

export default defineConfig({
  storage: new CloudinaryStorageAdapter({
    cloudName: process.env.CLOUDINARY_CLOUD_NAME!,
    apiKey:    process.env.CLOUDINARY_API_KEY!,
    apiSecret: process.env.CLOUDINARY_API_SECRET!,
    folder: 'my-project',       // Optional: upload into a specific folder
  }),
})

With Cloudinary, image imageSizes in your upload config are ignored — use Cloudinary URL transformations instead:

// In your frontend, use the Cloudinary URL transformation API
const thumbnailUrl = doc.url.replace('/upload/', '/upload/w_300,h_300,c_fill/')

Writing a Custom Adapter

Implement the StorageAdapter interface to support any storage backend:

import type { StorageAdapter } from '@dyrected/core'

class MyCustomStorage implements StorageAdapter {
  async upload(file: Buffer, options: {
    filename: string
    mimeType: string
    size: number
  }): Promise<{ url: string; filename: string }> {
    // Upload the file and return its public URL
    const url = await myUploadService.put(options.filename, file)
    return { url, filename: options.filename }
  }

  async delete(filename: string): Promise<void> {
    await myUploadService.remove(filename)
  }
}

export default defineConfig({
  storage: new MyCustomStorage(),
})

How url is Resolved

Always use doc.url from the API response — never hand-assemble paths:

Adapterurl value
LocalStorageAdapter/uploads/image.jpg
S3StorageAdapter (no CDN)https://bucket.s3.region.amazonaws.com/image.jpg
S3StorageAdapter (with baseUrl)https://cdn.mysite.com/image.jpg
CloudinaryStorageAdapterhttps://res.cloudinary.com/cloud-name/image/upload/image.jpg

For image sizes, each size's url is also pre-resolved:

{
  "url": "https://cdn.mysite.com/hero.jpg",
  "sizes": {
    "thumbnail": { "url": "https://cdn.mysite.com/hero-thumbnail.jpg" }
  }
}

Upload Config Reference

Configure the upload behaviour per collection (see also Upload Collections):

{
  slug: 'media',
  upload: {
    allowedMimeTypes: ['image/*', 'application/pdf'],
    maxFileSize: 10_000_000,
    imageSizes: [
      { name: 'thumbnail', width: 300, height: 300, crop: 'center' },
      { name: 'card',      width: 800, height: 450 },
      { name: 'hero',      width: 1920 },
    ],
    adminThumbnail: 'thumbnail',
  },
}
OptionTypeDefaultDescription
allowedMimeTypesstring[]allAllowed MIME type patterns. Supports image/* globs.
maxFileSizenumber7_000_000Max upload size in bytes
imageSizesImageSize[][]Resize configurations (see below)
adminThumbnailstringoriginalWhich imageSizes entry to use in the Admin media grid

ImageSize options

OptionTypeDescription
namestringUnique identifier used in the API response
widthnumberTarget width in pixels
heightnumberTarget height in pixels (optional)
cropstringCrop position: 'center', 'top', 'bottom', 'left', 'right'
fitstringsharp fit: 'cover', 'contain', 'fill', 'inside', 'outside'
withoutEnlargementbooleanDon't upscale images smaller than the target (default: true)
formatOptionsobjectsharp format options, e.g. { jpeg: { quality: 85 } }

On this page