Agora

Configuration

Every key on defineConfig — disk and the disks map, store and stores, imageProcessor, collections, uploads, delivery, the attachment key prefix and the diagnostics toggle — and how the provider builds the MediaManager at boot.

All configuration lives in config/media.ts, typed by the exported defineConfig helper. (The console has its own file, config/media_dashboard.ts — see Dashboard.) The shape follows the Agora config idiom: drivers live in the core package, you select them by name from a factory map, and each heavy peer (@adonisjs/lucid, sharp) is imported lazily — only when actually chosen.

config/media.ts
import { defineConfig, stores, processors } from '@adonis-agora/media'

export default defineConfig({
  disk: 's3',
  store: 'lucid',
  stores: {
    memory: stores.memory(),
    lucid: stores.lucid({ connection: 'pg' }),
  },
  imageProcessor: processors.sharp(),
  collections: [
    { name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
    {
      name: 'gallery',
      conversions: [
        { name: 'thumb', width: 200, height: 200 },
        { name: 'og', width: 1200, eager: true },
      ],
    },
  ],
})

The keys at a glance

KeyTypeDefaultWhat it controls
diskstringDrive's default diskName of the disk media is written to — resolved from disks first, then from @adonisjs/drive.
disksRecord<string, DiskFactory>Disks bundled with this package (e.g. disks.s3()). A name here wins over a Drive disk of the same name. See S3 disk.
storestringmemoryWhich entry of stores to use for metadata persistence.
storesRecord<string, StoreFactory>The named store factories (stores.memory(), stores.lucid()).
imageProcessorImageProcessor | ImageProcessorFactoryThe conversion engine. Required only if any collection has conversions.
collectionsMediaCollectionConfig[][]Collection definitions — see Collections & Conversions.
uploadsMediaUploadsConfigLarge-file uploads: default mode, part size, presign TTL, and the opt-in routes for direct, direct sessions and TUS.
deliveryMediaDeliveryConfig{ mode: 'auto' }How stored media is served back — auto / public / signed / proxy, plus the signed-URL TTL. See Delivery.
attachmentKeyPrefixstringattachmentsKey prefix for column attachments created via the AttachmentManager.
emitDiagnosticsbooleantrueWhether to emit agora:media:* diagnostics events.

Every key is optional. An empty defineConfig({}) gives you the in-memory store on Drive's default disk, no conversions, no upload routes, auto delivery, and diagnostics on.

The three sub-configs are large enough to have pages of their own: disks on S3 disk, uploads across the three upload pages, and delivery on Delivery. What follows covers the rest.


disk — where files live

disk is a disk name, resolved in two steps: first against this config's own disks map (the drivers bundled with this package, e.g. disks.s3()), and only then against your config/drive.ts. A name present in both resolves to the local one. Omit disk entirely and the provider falls back to Drive's configured default disk — the default key of config/drive.ts, resolved through Drive's config provider.

So there are two independent precedences. Which disk a write targets:

  1. a per-call disk passed to attach() (or createFromFile()),
  2. else the collection's disk,
  3. else this top-level disk,
  4. else Drive's default disk.

And where that name resolves from: the disks map first, @adonisjs/drive second. An app whose disks all come from disks never touches Drive at all — the Drive manager is read lazily, at the first resolution that actually needs it.

defineConfig({
  disk: 's3', // everything goes to s3 unless a collection or call overrides it
  collections: [
    { name: 'invoices', disk: 'private-s3' }, // this collection lives on a different disk
  ],
})

Because storage is just Drive, a media record stores which disk it lives on alongside its path. URL resolution, conversions and deletion all use the record's stored disk — so moving a collection to a new disk doesn't strand existing files.


store and stores — metadata persistence

The bytes go to Drive; the metadata (owner, collection, path, conversions, ordering) goes to a MediaStore. You build a named map with the stores factory and pick one with store.

import { defineConfig, stores } from '@adonis-agora/media'

export default defineConfig({
  store: 'lucid',
  stores: {
    memory: stores.memory(),
    lucid: stores.lucid({ connection: 'pg', table: 'media' }),
  },
})
  • stores.memory() — an in-memory store. Single-process and non-durable; ideal for tests and scratch apps. This is the default when no store is selected.
  • stores.lucid({ connection?, table? }) — persists rows in SQL via @adonisjs/lucid. connection defaults to Lucid's default connection; table defaults to media. Needs the published migration (node ace migration:run).

Each factory is a lazy thunk: calling stores.lucid(...) in the config file costs nothing — the @adonisjs/lucid peer is only imported when the provider builds the selected store at boot. So you can list lucid in the map even in an environment where Lucid isn't installed, as long as you don't select it.

See Stores & Processors for the Lucid schema and how to write your own store.


imageProcessor — the conversion engine

Conversions need an engine. imageProcessor accepts either:

  • an ImageProcessorFactory built with the processors factory — e.g. processors.sharp(), which imports the optional sharp peer lazily; or
  • a ready ImageProcessor instance, if you've built your own.
import { defineConfig, processors } from '@adonis-agora/media'

export default defineConfig({
  imageProcessor: processors.sharp(),
})

It is only required if some collection (or attachment call) actually defines conversions. If you never convert images, omit it — and asking for a conversion later will throw ImageProcessorMissingError rather than silently failing.


collections — the policy layer

Each entry defines a named collection's behaviour — MIME whitelist, single-file replacement, per-collection disk, and conversion presets:

defineConfig({
  collections: [
    { name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
    {
      name: 'gallery',
      conversions: [
        { name: 'thumb', width: 200, height: 200 },
        { name: 'og', width: 1200, eager: true },
      ],
    },
  ],
})

This whole surface is documented on Collections & Conversions.

Collections are optional. Attaching to a collection that isn't declared is allowed — it falls back to a permissive default (no MIME whitelist, not single, no conversions, default disk). Declare a collection only when you want to constrain it.


attachmentKeyPrefix and emitDiagnostics

  • attachmentKeyPrefix (default attachments) is the storage key prefix used by the column-attachment layer. Files created via media.attachments.createFromFile(...) land under ${prefix}/${id}/.... A per-call keyPrefix overrides it.
  • emitDiagnostics (default true) toggles the agora:media:* lifecycle events. See Diagnostics below.

How the provider builds it

At boot, MediaProvider binds a singleton MediaManager assembled from this config:

  1. It resolves disks from the booted Drive manager via a lazy import('@adonisjs/drive/services/main') — no hard import of Drive.
  2. It builds the selected store from the stores map. Naming a store with no matching factory throws StoreNotConfiguredError — it does not quietly fall back to the in-memory store, because a typo silently swapping durable persistence for a process-local Map is the kind of bug you discover after losing data. Only the zero-config path (no store named at all) resolves to in-memory. The same no-silent-fallback rule holds for the upload-session stores.
  3. It resolves the imageProcessor — calling the factory thunk if you passed one, or using the instance directly.
  4. It computes the default disk: your disk, else Drive's default.

The result is one MediaManager shared across your app, composing the MediaLibrary and AttachmentManager over the same StorageManager.


Diagnostics

The library emits lifecycle events on the agora:media:* channel through the @agora/diagnostics:emit global slot — read structurally, with no hard dependency on @adonis-agora/diagnostics. The events are:

EventChannelPayload highlights
attachagora:media:attachid, ownerType, ownerId, collection, disk, path, size, mimeType
deleteagora:media:deleteid, ownerType, ownerId
conversionagora:media:conversionid, conversion, path
attachment.createagora:media:attachment.createdisk, path, size, mimeType, name, variants
attachment.deleteagora:media:attachment.deletedisk, path, variants
upload.startagora:media:upload.startid, disk, key, mode (proxy | direct), size?, contentType?
upload.progressagora:media:upload.progressid, offset (bytes safely stored), parts, size?
upload.completeagora:media:upload.completeid, disk, key
upload.abortagora:media:upload.abortid, disk, key

The four upload.* events cover every upload path — proxy, direct, direct sessions and TUS alike — so one subscriber sees them all. offset on upload.progress is the resume offset: bytes that are genuinely durable, not bytes merely received.

When @adonis-agora/diagnostics isn't installed, the slot is empty and emitting is an inert no-op. When it is installed, the Telescope generic watcher captures every media event automatically. Emitting never throws back into a media operation, and you can turn it off entirely with emitDiagnostics: false.


Next steps

On this page