Dyrected
Model ContentMedia

Storage Adapters

Storage adapters decide where uploaded files live and how their URLs are resolved.

Use this page when you already understand upload collections and now need to decide where the file bytes should live. By the end, you should know which adapters Dyrected currently ships, which environment variables the CLI actually scaffolds, and which storage concerns belong here instead of on the upload collection page.

import { defineConfig } from '@dyrected/core'
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_ID!,
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
    },
  }),
  collections: [Media],
  globals: [],
})

What a storage adapter does

A storage adapter persists file bytes and resolves the public URL that upload documents expose through doc.url. Always consume that resolved URL rather than reconstructing storage paths yourself. Local storage serves bytes from your app's own upload path, while cloud adapters usually return a provider or CDN URL directly.

The split is simple:

  • the upload collection decides which files are allowed and what metadata to keep
  • the storage adapter decides where files live and how their URLs resolve

If your collection also defines imageSizes, the root-level image service handles the processing step for those generated variants.

Choosing storage

The recommended path depends on how you are running Dyrected:

  • Dyrected Cloud: do not configure a custom storage adapter unless your project setup explicitly requires one. Cloud manages storage for you.
  • Local development or one durable server: use localStorage.
  • Self-hosted production or multiple app instances: use an object store through s3Storage or b2Storage.
  • Provider-owned media pipeline: use cloudinaryStorage when Cloudinary should own delivery URLs and transformations.

Local filesystem

Use local storage when the app and the uploaded files live on the same durable disk. This is the simplest path for local development and for single-node deployments with persistent storage.

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

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

This path does not need storage-specific environment variables. It is not the right choice for ephemeral filesystems or multi-instance deployments unless those instances share the same mounted volume.

S3 and other S3-compatible object storage

Use s3Storage when you need shared object storage. This is the general self-hosted production path.

import { defineConfig } from '@dyrected/core'
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_ID!,
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
    },
    endpoint: process.env.S3_ENDPOINT,
    forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
    baseUrl: process.env.S3_BASE_URL,
  }),
  collections: [Media],
  globals: [],
})

If you scaffold a self-hosted app with dyrected init and choose s3, the current CLI writes these variables into .env.example:

  • S3_BUCKET
  • S3_REGION
  • S3_ACCESS_KEY_ID
  • S3_SECRET_ACCESS_KEY

The adapter also supports runtime-only options that the CLI does not scaffold for you:

  • S3_ENDPOINT if your S3-compatible provider needs a custom API endpoint
  • S3_FORCE_PATH_STYLE if that provider requires path-style URLs
  • S3_BASE_URL if you serve files through a custom public origin or CDN

Keep the credentials server-side. Frontend code should consume the returned doc.url, not your bucket naming scheme.

Backblaze B2

Use b2Storage when you want the native B2 adapter instead of going through an S3-compatible layer.

import { defineConfig } from '@dyrected/core'
import { b2Storage } from '@dyrected/storage-b2'

export default defineConfig({
  storage: b2Storage({
    bucketId: process.env.B2_BUCKET_ID!,
    bucketName: process.env.B2_BUCKET_NAME!,
    applicationKeyId: process.env.B2_KEY_ID!,
    applicationKey: process.env.B2_APPLICATION_KEY!,
    baseUrl: process.env.B2_BASE_URL,
  }),
  collections: [Media],
  globals: [],
})

If you scaffold with dyrected init and choose b2, the CLI writes:

  • B2_BUCKET_ID
  • B2_BUCKET_NAME
  • B2_KEY_ID
  • B2_APPLICATION_KEY

The runtime adapter also supports baseUrl, which is useful when your files are delivered through a custom domain or CDN. The CLI does not currently scaffold that variable for you.

Cloudinary

Use cloudinaryStorage when Cloudinary should own the delivery URL and media pipeline.

import { defineConfig } from '@dyrected/core'
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: process.env.CLOUDINARY_FOLDER,
  }),
  collections: [Media],
  globals: [],
})

If you scaffold with dyrected init and choose cloudinary, the CLI writes:

  • CLOUDINARY_CLOUD_NAME
  • CLOUDINARY_API_KEY
  • CLOUDINARY_API_SECRET

The adapter also supports folder, but the CLI does not scaffold a matching env var for it today. Add one yourself if you want the adapter to prefix uploads under a specific Cloudinary folder.

URL resolution and serving

Always treat doc.url as the public contract.

  • local storage builds a root-relative URL from staticUrlPrefix
  • cloud adapters usually return a provider or CDN URL
  • resolve() only matters for adapters that need Dyrected to serve the file bytes itself

That is why frontend code should not guess at filenames, prefixes, or provider domains. The adapter is allowed to change those decisions without changing the document shape.

Keep collection-level upload rules on Upload overview. Things like allowedMimeTypes, maxFileSize, and imageSizes are content-model decisions, not storage-provider decisions. This page is for choosing an adapter, wiring credentials, and understanding how doc.url is produced.

Generated reference

The contracts below are generated from the public @dyrected/core exports by @dyrected/knowledge, so the storage, file-data, and image-service interfaces stay in sync with the package. Use them when you are implementing a custom adapter or checking the exact argument and return shapes.

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;
}
OptionDescription
filename (required)
filesize (optional)
mimeType (required)
url (required)Public URL of the stored file.
width (optional)
height (optional)
focalPoint (optional)
blurhash (optional)Base64-encoded BlurHash string for progressive image loading.
type (optional)`'upload'` for server-stored files; `'external'` for provider-managed files.
provider (optional)
provider_metadata (optional)

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 }>;
  }>;
}
OptionDescription
process (optional)

StorageAdapter

The interface every storage adapter must implement.

Dyrected ships adapters for local disk, AWS S3 and other S3-compatible services through the S3 adapter, 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>;
}
OptionDescription
upload (optional)Upload a file and return its metadata (URL, dimensions, etc.). The `prefix` is a path prefix used for multi-tenant setups.
delete (required)Delete a file by its stored filename.
getURL (required)Return the public URL for a stored file.
resolve (required)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.

Custom adapters and escape hatches

When a shipped adapter does not match your infrastructure, implement StorageAdapter yourself and return a complete FileData object from upload.

The main escape hatches are:

  • custom public origins through baseUrl-style options where the shipped adapter supports them
  • a provider-specific custom adapter when your storage API is not covered by the shipped packages
  • a custom resolve() implementation when Dyrected, not the provider, must serve the bytes

If you go that route, update the generated contract source instead of hand-maintaining parallel interface docs.

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend