cloud_queue
Better MediaDeveloper docs

Plugin Architecture

How files move through the pipeline, when each plugin runs, and how to write a custom plugin.

Every file uploaded through Better Media passes through a fixed sequence of five phases. Built-in and custom plugins hook into these phases to validate, scan, and transform files.

Pipeline Phases

upload:init  →  validation:run  →  scan:run  →  process:run  →  upload:complete
PhaseWhat happensBuilt-in plugins
upload:initFile is written to storage. Initial DB record created.
validation:runFile type, size, and content are verified. Can abort the upload.Validation plugin
scan:runFile is scanned for threats. Can abort the upload.Virus scan plugin
process:runDerivatives are generated (thumbnails, compressed versions).Media processing plugin
upload:completeStatus updated to ready. Completion events fired.

File Status

The status field on a media record tracks where it is in the pipeline:

StatusMeaning
pendingUpload started, not yet processed.
processingPipeline is running.
readyAll phases passed. File is usable.
failedValidation or scan rejected the file.
quarantinedVirus detected.

Sync vs Background

Each plugin runs in one of two modes:

Sync — runs during the upload call. The upload response waits for it to finish. Can reject the file (validation and scan phases only).

Background — the upload call returns immediately. Work is queued and runs in a worker process later. Use this for slow operations like image processing.

validationPlugin({ executionMode: "sync" })      // blocks upload, can abort
mediaProcessingPlugin({ executionMode: "background" }) // upload returns, thumbnails generated after

Writing a Custom Plugin

Plugins implement a simple apply(runtime) method and tap into whichever phase they need:

import type { PipelinePlugin, MediaRuntime } from "@better-media/core";

export class WatermarkPlugin implements PipelinePlugin {
  readonly name = "watermark";
  readonly runtimeManifest = {
    id: "my-watermark-plugin",
    version: "1.0.0",
    namespace: "watermark",
    trustLevel: "untrusted" as const,
    capabilities: ["file.read", "metadata.write.own"] as const,
  };
  // id and namespace must be unique across all registered plugins

  apply(runtime: MediaRuntime): void {
    runtime.hooks["process:run"].tap(
      "watermark",
      async (context, api) => {
        const buf = context.utilities?.fileContent?.buffer;
        if (!buf) return;

        const watermarked = await applyWatermark(buf);
        await context.storage.put(context.file.key, watermarked);

        api.emitMetadata({ watermarked: true });
      },
      { mode: "sync" }
    );
  }
}

Register it alongside the built-in plugins:

const media = createBetterMedia({
  plugins: [
    validationPlugin({ ... }),
    new WatermarkPlugin(),
  ],
});

Writing to the Pipeline

Inside a handler, use the api argument — never mutate context directly.

MethodWhat it does
api.emitMetadata({ key: value })Stores metadata under your plugin's namespace. Readable from context.metadata.
api.emitProcessing({ key: value })Stores processing outputs (e.g. derivative keys). Readable from context.processing.

Reading the File

The file bytes are available in context.utilities?.fileContent:

const buf = context.utilities?.fileContent?.buffer;
const tmp = context.utilities?.fileContent?.tempPath; // set for large files streamed to disk

const bytes = buf ?? (tmp ? await fs.readFile(tmp) : null);

On this page