Dyrecteddyrected
Adapters

Storage Adapters

Configure file storage, public URLs, uploads, and image processing.

Dyrected delegates file persistence to a storage adapter. The adapter receives bytes, stores them, and returns canonical metadata such as filename, mimeType, and url. Configure one adapter globally and use upload-enabled collections for file metadata and editorial fields.

Dyrected Cloud manages storage and delivery. Cloud projects normally omit a custom storage adapter.

Local filesystem

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

export default defineConfig({
  storage: localStorage({
    uploadDir: './public/uploads',
    staticUrlPrefix: '/uploads',
  }),
  collections: [],
  globals: [],
})

Local storage is appropriate for development or one durable server. It is not appropriate for ephemeral serverless filesystems or multiple instances without a shared volume.

S3 and compatible providers

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

export default defineConfig({
  storage: s3Storage({
    bucket: process.env.S3_BUCKET!,
    region: process.env.S3_REGION!,
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY!,
      secretAccessKey: process.env.S3_SECRET_KEY!,
    },
    endpoint: process.env.S3_ENDPOINT,
    forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
    baseUrl: process.env.CDN_URL,
  }),
  collections: [],
  globals: [],
})

Use endpoint for services such as R2, Spaces, B2 S3 compatibility, or MinIO. Use forcePathStyle only when the provider requires it. baseUrl should be the public CDN or custom-domain origin without a trailing object path.

Keep credentials server-only and grant only the bucket permissions the adapter needs.

Cloudinary

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

export default defineConfig({
  storage: cloudinaryStorage({
    cloudName: process.env.CLOUDINARY_CLOUD_NAME!,
    apiKey: process.env.CLOUDINARY_API_KEY!,
    apiSecret: process.env.CLOUDINARY_API_SECRET!,
    folder: 'my-project',
  }),
  collections: [],
  globals: [],
})

Cloudinary owns transformation and delivery behavior. Prefer provider transformations when they are the canonical source of variants; use Dyrected image processing when you need generated sizes across storage providers.

Backblaze B2

Use b2Storage from @dyrected/storage-b2 when using the native B2 API. Supply a public baseUrl when the bucket is delivered through a custom domain or CDN.

URL resolution

Always consume the returned doc.url. Do not hand-assemble storage paths in frontend code: providers can change key prefixes, public domains, or delivery strategies without changing the document contract.

Local storage can implement resolve() so Dyrected serves bytes through /api/media/{filename}. Cloud adapters normally return direct public URLs and do not need resolve().

Upload collection configuration

export const Media = defineCollection({
  slug: 'media',
  upload: {
    allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
    maxFileSize: 10 * 1024 * 1024,
    imageSizes: [
      { name: 'thumbnail', width: 300, height: 300, fit: 'cover' },
      { name: 'card', width: 800, withoutEnlargement: true },
    ],
    adminThumbnail: 'thumbnail',
  },
  fields: [
    { name: 'alt', type: 'text', label: 'Alternative text' },
    { name: 'caption', type: 'textarea', label: 'Caption' },
  ],
})

MIME validation is a boundary check, not a substitute for validating untrusted file contents. Generated image sizes require an ImageService, such as the supported Sharp integration.

Writing a custom adapter

Implement StorageAdapter from @dyrected/core using the object argument shapes in the generated contract. Return the provider-resolved public URL from upload and getURL. Implement resolve only when Dyrected must retrieve private bytes itself.

Test key normalization, deletion, Unicode filenames, missing files, MIME metadata, and prefix isolation. Avoid importing internal workspace source paths.

Generated storage contracts

FileData

Metadata returned after a file is uploaded and stored. Stored on the document in upload collections.

export interface FileData {
  filename: string;
  filesize?: number;
  mimeType: string;
  /** Public URL of the stored file. */
  url: string;
  width?: number;
  height?: number;
  focalPoint?: { x: number; y: number };
  /** Base64-encoded BlurHash string for progressive image loading. */
  blurhash?: string;
  /** `'upload'` for server-stored files; `'external'` for provider-managed files. */
  type?: "upload" | "external";
  provider?: string;
  provider_metadata?: unknown;
  [key: string]: unknown;
}
MemberSignatureDescription
filenamefilename: string
filesizefilesize?: number
mimeTypemimeType: string
urlurl: stringPublic URL of the stored file.
widthwidth?: number
heightheight?: number
focalPointfocalPoint?: { x: number; y: number }
blurhashblurhash?: stringBase64-encoded BlurHash string for progressive image loading.
typetype?: "upload" | "external"`'upload'` for server-stored files; `'external'` for provider-managed files.
providerprovider?: string
provider_metadataprovider_metadata?: unknown

ImageService

Processes uploaded images — generates metadata (dimensions, BlurHash) and produces resized variants defined in UploadConfig.imageSizes.

export interface ImageService {
  process(args: {
    buffer: Uint8Array;
    mimeType: string;
    config?: boolean | UploadConfig;
    focalPoint?: { x: number; y: number };
  }): Promise<{
    metadata: {
      width?: number;
      height?: number;
      /** Base64-encoded BlurHash for progressive loading. */
      blurhash?: string;
    };
    /** Generated image sizes keyed by their `name`. */
    sizes?: Record<string, { buffer: Uint8Array; width: number; height: number; filename: string }>;
  }>;
}
MemberSignatureDescription
processprocess(args: { buffer: Uint8Array; mimeType: string; config?: boolean | UploadConfig; focalPoint?: { x: number; y: number }; }): Promise<{ metadata: { width?: number; height?: number; /** Base64-encoded BlurHash for progressive loading. / blurhash?: string; }; /* Generated image sizes keyed by their `name`. */ sizes?: Record<string, { buffer: Uint8Array; width: number; height: number; filename: string }>; }>

StorageAdapter

The interface every storage adapter must implement.

Dyrected ships adapters for local disk, S3, Cloudflare R2, Cloudinary, and Backblaze B2. Implement this interface to use any other storage provider.

export interface StorageAdapter {
  /**
   * Upload a file and return its metadata (URL, dimensions, etc.).
   * The `prefix` is a path prefix used for multi-tenant setups.
   */
  upload(args: { filename: string; buffer: Uint8Array; mimeType: string; prefix?: string }): Promise<FileData>;

  /** Delete a file by its stored filename. */
  delete(args: { filename: string }): Promise<void>;

  /** Return the public URL for a stored file. */
  getURL(args: { filename: string }): string;

  /**
   * Retrieve the file's raw bytes and MIME type for serving via the API.
   * Only needed by adapters that serve files through the Dyrected API
   * (e.g. `LocalStorage`). Cloud adapters return `null` here and rely on
   * direct CDN URLs instead.
   */
  resolve?(args: { filename: string }): Promise<{ buffer: Uint8Array; mimeType: string } | null>;
}
MemberSignatureDescription
uploadupload(args: { filename: string; buffer: Uint8Array; mimeType: string; prefix?: string }): Promise<FileData>Upload a file and return its metadata (URL, dimensions, etc.). The `prefix` is a path prefix used for multi-tenant setups.
deletedelete(args: { filename: string }): Promise<void>Delete a file by its stored filename.
getURLgetURL(args: { filename: string }): stringReturn the public URL for a stored file.
resolveresolve(args: { filename: string }): Promise<{ buffer: Uint8Array; mimeType: string } | null>Retrieve the file's raw bytes and MIME type for serving via the API. Only needed by adapters that serve files through the Dyrected API (e.g. `LocalStorage`). Cloud adapters return `null` here and rely on direct CDN URLs instead.

On this page