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.
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
| Key | Type | Default | What it controls |
|---|---|---|---|
disk | string | Drive's default disk | Name of the disk media is written to — resolved from disks first, then from @adonisjs/drive. |
disks | Record<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. |
store | string | memory | Which entry of stores to use for metadata persistence. |
stores | Record<string, StoreFactory> | — | The named store factories (stores.memory(), stores.lucid()). |
imageProcessor | ImageProcessor | ImageProcessorFactory | — | The conversion engine. Required only if any collection has conversions. |
collections | MediaCollectionConfig[] | [] | Collection definitions — see Collections & Conversions. |
uploads | MediaUploadsConfig | — | Large-file uploads: default mode, part size, presign TTL, and the opt-in routes for direct, direct sessions and TUS. |
delivery | MediaDeliveryConfig | { mode: 'auto' } | How stored media is served back — auto / public / signed / proxy, plus the signed-URL TTL. See Delivery. |
attachmentKeyPrefix | string | attachments | Key prefix for column attachments created via the AttachmentManager. |
emitDiagnostics | boolean | true | Whether 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:
- a per-call
diskpassed toattach()(orcreateFromFile()), - else the collection's
disk, - else this top-level
disk, - 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 nostoreis selected.stores.lucid({ connection?, table? })— persists rows in SQL via@adonisjs/lucid.connectiondefaults to Lucid's default connection;tabledefaults tomedia. 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
ImageProcessorFactorybuilt with theprocessorsfactory — e.g.processors.sharp(), which imports the optionalsharppeer lazily; or - a ready
ImageProcessorinstance, 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(defaultattachments) is the storage key prefix used by the column-attachment layer. Files created viamedia.attachments.createFromFile(...)land under${prefix}/${id}/.... A per-callkeyPrefixoverrides it.emitDiagnostics(defaulttrue) toggles theagora:media:*lifecycle events. See Diagnostics below.
How the provider builds it
At boot, MediaProvider binds a singleton MediaManager assembled from this config:
- It resolves disks from the booted Drive manager via a lazy
import('@adonisjs/drive/services/main')— no hard import of Drive. - It builds the selected store from the
storesmap. Naming astorewith no matching factory throwsStoreNotConfiguredError— it does not quietly fall back to the in-memory store, because a typo silently swapping durable persistence for a process-localMapis the kind of bug you discover after losing data. Only the zero-config path (nostorenamed at all) resolves to in-memory. The same no-silent-fallback rule holds for the upload-session stores. - It resolves the
imageProcessor— calling the factory thunk if you passed one, or using the instance directly. - It computes the default disk: your
disk, else Drive'sdefault.
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:
| Event | Channel | Payload highlights |
|---|---|---|
attach | agora:media:attach | id, ownerType, ownerId, collection, disk, path, size, mimeType |
delete | agora:media:delete | id, ownerType, ownerId |
conversion | agora:media:conversion | id, conversion, path |
attachment.create | agora:media:attachment.create | disk, path, size, mimeType, name, variants |
attachment.delete | agora:media:attachment.delete | disk, path, variants |
upload.start | agora:media:upload.start | id, disk, key, mode (proxy | direct), size?, contentType? |
upload.progress | agora:media:upload.progress | id, offset (bytes safely stored), parts, size? |
upload.complete | agora:media:upload.complete | id, disk, key |
upload.abort | agora:media:upload.abort | id, 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
- Collections & Conversions — the
collectionssurface in depth - Stores & Processors — the Lucid store, the sharp processor, and custom drivers
- Upload modes — the
uploadskey in depth - Delivery — the
deliverykey in depth - Attachments —
attachmentKeyPrefixin context
Getting Started
Install @adonisjs/drive and @adonis-agora/media, configure the library, attach your first file to an entity, generate a conversion, and resolve URLs.
Collections & Conversions
Collections are the policy layer — MIME whitelist, single-file replacement, ordering, and a per-collection disk. Conversions are the image pipeline — width/height/fit/format/quality, generated eagerly or lazily and cached.