Agora

Media

A media library for AdonisJS — attach files to your entities, organize them into collections, and generate image conversions, all on top of @adonisjs/drive. The spatie/laravel-media-library feel, for Adonis.

@adonis-agora/media gives AdonisJS the spatie/laravel-media-library feel: attach files to your entities, organize them into named collections (with a MIME whitelist, single-file replacement, and ordering), and generate image conversions — eagerly on upload or lazily on first access. Storage itself is delegated entirely to @adonisjs/drive — either major, ^3.0.0 or ^4.0.0 — so you reuse your existing local / s3 / gcs disks. This package never reimplements disk drivers.

The problem it solves

Handling uploads in a real app means more than calling file.move(). You end up rebuilding the same machinery in every project: where does this file live, which MIME types are allowed here, is this the kind of field that holds one file (an avatar) or many (a gallery), how do I produce a thumbnail, and how do I clean everything up when the row is deleted. @adonis-agora/media collapses that into two small layers:

  • A media libraryattach a file to an entity in a named collection, list / find / delete it, and ask for a url. Collections carry the policy (allowed MIME types, single-vs-many, conversions); the library enforces it.
  • Column attachments — the adonis-attachment style, where a file lives directly on a model column as a JSON value (plus any image variants). No extra table, no owner — just a field.

Both sit over the same Drive disks and the same image processor, so you pick whichever shape fits each field and the storage story is identical.

Built on Drive, with pluggable backends

The library talks to storage through a tiny structural Disk contract that a real @adonisjs/drive disk satisfies directly — so it never imports Drive and Drive stays a peer dependency you already have. Two more seams are pluggable and lazy:

  • The MediaStore persists the metadata rows. Ships with an in-memory store (single-process, for tests and scratch apps) and a Lucid store (SQLite / Postgres / MySQL, with a published migration). The Lucid driver only imports @adonisjs/lucid when you actually select it.
  • The ImageProcessor does the resizing / cropping / format conversion. Ships with a sharp processor; sharp is an optional peer imported only when conversions are configured.

This is the Agora config idiom: drivers live in the core package, you select them by name with a defineConfig + factory map, and each heavy peer loads lazily only when chosen.

Quickstart

The minimal loop — install Drive, install and configure media, attach a file, get a URL.

Install the storage backend and the library, then run the configure codemod:

node ace add @adonisjs/drive   # storage backend (required)
npm i @adonis-agora/media
node ace configure @adonis-agora/media

configure registers both providers (the library and the embedded management console), publishes config/media.ts and config/media_dashboard.ts, and publishes two Lucid migrations — the media table and the upload-session table. Delete the migrations you don't need; the in-memory stores need none.

Import the media service and attach an uploaded file to an entity's collection from a controller:

app/controllers/post_images_controller.ts
import { createReadStream } from 'node:fs'
import media from '@adonis-agora/media/services/main'
import type { HttpContext } from '@adonisjs/core/http'

export default class PostImagesController {
  async store({ request, response, params }: HttpContext) {
    const image = request.file('image', { size: '5mb', extnames: ['jpg', 'png', 'webp'] })
    if (!image) return response.badRequest({ error: 'An "image" file is required' })

    const record = await media.library.attach({
      ownerType: 'Post',
      ownerId: params.id,
      collection: 'gallery',
      fileName: image.clientName,
      mimeType: image.type!,
      contents: createReadStream(image.tmpPath!), // Buffer | Readable
    })

    return response.created({ id: record.id })
  }
}

Read it back — the original or a (lazily generated, then cached) conversion:

await media.library.url(record.id)            // public url of the original
await media.library.url(record.id, 'thumb')   // generated on first call, then cached
await media.library.list('Post', params.id, 'gallery')
await media.library.delete(record.id)         // removes the original AND every conversion

For the full walkthrough — including the optional for(owner) binding and column attachments — see Getting Started.

What you get

  • Collections with policy. A collection is a named bucket with an optional MIME whitelist, an optional single: true (attaching replaces the existing file), per-collection conversions, and a per-collection disk override.
  • Image conversions, eager or lazy. Declare presets (width / height / fit / format / quality); eager ones are generated on attach, the rest on the first url(id, name) and cached on the record so they're produced once.
  • Two attachment shapes over one storage layer. The owner-collection MediaLibrary and the column AttachmentManager, sharing the same disks and image processor.
  • Reuse your Drive disks. local, s3, gcs — whatever you configured in config/drive.ts. The default disk, a per-collection disk, and a per-attach disk override all compose.
  • Pluggable persistence. An in-memory store for tests and a portable Lucid store with a published migration; bring your own MediaStore by implementing one interface.
  • A testing kit. In-memory doubles for all three SPIs (InMemoryMediaStore, InMemoryDisk, FakeImageProcessor) so you can exercise the library with no disk, database, or sharp.
  • Structural diagnostics. Lifecycle events on agora:media:* via a global slot, captured automatically by Telescope when present — and an inert no-op when it isn't.

Where to go next

Getting Started

Install Drive, configure the library, attach your first file, and resolve URLs — step by step.

Configuration

Every key on defineConfig — disk, store/stores, imageProcessor, collections, the diagnostics toggle.

Collections & Conversions

MIME whitelist, single-file replace, ordering, and image presets — eager vs lazy generation.

Attachments

Column attachments (adonis-attachment style): a file (and variants) stored directly on a model column.

Single-file store

The avatar-style seam — replace this owner's one file, get a URL, with no hard dependency on media.

Upload modes

Large-file uploads — proxy (through your app) and direct-S3 multipart (straight to the bucket).

Resumable / TUS

Chunked, resumable uploads over the tus protocol, with a pluggable session store.

Direct upload policy

The app-injected seam deciding an upload's key, what a completed upload becomes, and how failures read as HTTP.

S3 disk

The bundled disks.s3() driver — extended operations, native multipart, presigned URLs.

React client

@adonis-agora/media-react — useMediaUpload, MediaUploader, and a framework-free upload client.

Dashboard

Embedded management console — browse buckets, watch uploads, copy/move/delete objects. No separate install.

Console authentication

The console's built-in session-cookie login — mint from your app's auth, or a login screen of its own.

Stores & Processors

The Lucid store and its migration, the sharp processor, the Disk contract, and writing your own driver.

Telescope

mediaTelescopeExtension — a Media overview dashboard built from the agora:media:* events.

Errors

Every exported error class, its stable code, and the properties it carries — branch on a code, never a message.

Testing

Drive the library with in-memory doubles — no disk, no database, no sharp.

Roadmap

What the library covers today, and what is still intentionally deferred.

On this page