Skip to content

Configuration ​

Everything lives in config/attachment.ts, built with defineConfig. The only required field is storage.

ts
// config/attachment.ts - generated by `node ace configure @jrmc/adonis-attachment`
import { defineConfig, LocalFileStorage } from '@jrmc/adonis-attachment'

export default defineConfig({
  storage: LocalFileStorage.fromApp,
})

A common adapter pattern ​

storage, store, and queue each accept an adapter instance or a synchronous/asynchronous factory (app, context) => adapter. Built-in adapters offer fromApp when defaults are sufficient and configure(options) when you need overrides:

ts
import { defineConfig, AdonisDriveStorage, MemoryAttachmentQueue } from '@jrmc/adonis-attachment'
import { LucidAttachmentStore } from '@jrmc/adonis-attachment/lucid'

export default defineConfig({
  storage: AdonisDriveStorage.configure({ disk: 's3' }),
  store: LucidAttachmentStore.configure({ tableName: 'media_attachments' }),
  queue: MemoryAttachmentQueue.configure({ concurrency: 2 }),
})

The default forms are AdonisDriveStorage.fromApp, LucidAttachmentStore.fromApp, and MemoryAttachmentQueue.fromApp. configure() returns a factory; it does not initialize an adapter immediately. Adonis Queue requires an application job, so use AdonisAttachmentQueue.configure({ job: GenerateAttachmentVariants }).

SettingResponsibilityWhen omitted
storageFile bytes and storage URLsRequired
storeAttachment records and processing persistenceDisabled; false also disables it
queueJob dispatch or in-process executionMemory queue

Factories resolve in this order: storage, store, queue. All receive defaults and variant. Storage receives the explicitly configured defaultDisk, if any. Store and queue receive the resolved disk: root defaultDisk, then the storage adapter's default, then fs. The queue context also supplies an optional handler, assembled from the store or overridden by jobHandler. A custom in-process queue can use it; external adapters usually dispatch jobs and build their processor in the worker instead.

Public types include AttachmentAdapterFactory, AttachmentStorageConfig, AttachmentStoreConfig, AttachmentQueueAdapterConfig and their role-specific contexts. Existing one-argument factories, queue driver shortcuts and named connections remain supported. Do not resolve jrmc.attachment or run the handler inside a config factory: configuration must finish before processing starts. Reuse stateful adapters per application when factories can be resolved for multiple bindings.

Storage ​

Storage decides where the bytes go. Pick one.

Import the built-in storage classes from the package root, or use the dedicated entry points @jrmc/adonis-attachment/storages/local-file and @jrmc/adonis-attachment/storages/adonis-drive. The former /adapters/... entry points are replaced by /storages/... in beta.2.

Local filesystem (default) ​

ts
storage: LocalFileStorage.fromApp,

LocalFileStorage writes under storage/attachments, on the fs disk. It creates parent directories, writes atomically (temp file + rename), and refuses paths that escape its root. Pass baseUrl when those files are exposed publicly and you need public URLs:

ts
storage: LocalFileStorage.configure({
  baseUrl: 'https://app.example.test/uploads',
})

Adonis Drive (S3, GCS, ...) ​

Install @adonisjs/drive, configure config/drive.ts, then:

ts
import { AdonisDriveStorage, defineConfig } from '@jrmc/adonis-attachment'

export default defineConfig({
  storage: AdonisDriveStorage.fromApp, // uses your default Drive disk
})

Your own backend ​

Any object implementing AttachmentStorage (write, read, remove) works. The fallback disk name is fs unless your adapter exposes defaultDisk or you set defaultDisk explicitly.

To override the default Drive disk, use storage: AdonisDriveStorage.configure({ disk: 's3' }).

Store ​

storage handles file bytes. store handles attachment data and the persistence of background-processing results. The two choices are independent.

Lucid is never detected automatically. Select it explicitly:

ts
import { defineConfig, AdonisDriveStorage } from '@jrmc/adonis-attachment'
import { LucidAttachmentStore } from '@jrmc/adonis-attachment/lucid'

export default defineConfig({
  storage: AdonisDriveStorage.fromApp,
  store: LucidAttachmentStore.fromApp,
})

Use LucidAttachmentStore.configure({ tableName: 'media_attachments' }) for custom tables, or LucidAttachmentStore.configure({ legacy: { models: { users: () => import('#models/user') } } }) for legacy JSON fields. Both options can be combined; tables and JSON can coexist.

Omitting store, or setting store: false, leaves data persistence to the application. No read route or automatic memory processing is enabled in that mode. Draft creation, file storage, synchronous metadata extraction and manual processing remain available.

For another ORM, provide an AttachmentStore object or a factory (app, context) => store. The context contains defaultDisk, defaults, and variant. Capabilities are optional:

Store memberPurpose
repositoryRequired. Read originals and variants by ID; optionally resolve contextual references. Supplies the repository for explicit router.attachments() routes.
persistenceOptional. Existing AttachmentPersistence lifecycle/transaction contract, for application-owned lifecycle integration.
metadataPersisterRequired for media.metadataPolicy.mode: 'deferred'. Persists extracted metadata.
variants(service, converters, context)Required to persist generated variants. Returns a VariantGenerator, including the adapter's replacement and cleanup policy.

The configured store is used by memory processing and by createAttachmentProcessor(app) in external workers. A read-only store is supported; requesting unsupported processing fails explicitly. Providing a store does not create another ORM's model decorators or schedule jobs before the application's transaction commits. Follow Creating a store for a complete class and factory, then custom persistence for lifecycle and transaction details.

Migrating from earlier v6 betas ​

Previous configurationReplacement
route: falseOmit router.attachments() in start/routes.ts
route: { prefix } or automatic read routeDeclare router.attachments('/media/:id/:name?') and select allowed disks
Automatic Lucid detectionstore: LucidAttachmentStore.fromApp
integrations.lucid.tableNamestore: LucidAttachmentStore.configure({ tableName })
integrations.legacy.modelslegacy.models inside LucidAttachmentStore.configure()
Root repositorystore.repository
media.metadataPersisterstore.metadataPersister
Root processorStore processing capabilities, or jobHandler: () => (job) => processor.process(job) for a fully custom processor

The removed keys fail with a migration error. queue, converters, events, defaults, variant, and media extraction settings keep their roles. No data or schema migration is needed solely for this configuration change.

Default persistence options ​

defaults sets the lowest-priority file options. They're resolved per setting, from most to least specific:

createFrom*(...) options  >  decorator options  >  defaults

Set an option to null at a higher level to clear an inherited value. This restores the package fallback; it is not equivalent to false (for example, rename: null restores generated names, while rename: false uses the client filename).

ts
export default defineConfig({
  storage: LocalFileStorage.fromApp,
  store: LucidAttachmentStore.fromApp,
  defaults: {
    folder: 'uploads',
    rename: true,          // store under a generated name instead of the client name
    normalizeFileName: true, // make supplied filenames portable across storage backends
    variants: ['thumbnail'],
  },
  variant: { basePath: 'variants/:id' }, // prefix for all generated variants
})

Persistence defaults accept disk, folder, rename, normalizeFileName, meta, preComputeUrl, and variants. variant is a separate root configuration section.

OptionAccepted valueFallback when unsetScope
diskStorage disk nameStorage adapter defaultConfig defaults, decorator, manager
folderRelative path or async callbackStorage rootConfig defaults, decorator, manager
renameBoolean or async filename callbacktrueConfig defaults, decorator, manager
normalizeFileNameBooleantrueConfig defaults, decorator, manager
metaBooleanExtraction not requestedConfig defaults, decorator, manager
preComputeUrlBooleanfalseSame priority; consumed on Lucid relation reads
variantsArray of configured keysNo automatic variantsSame priority; automatic scheduling with Lucid
variant.basePathRelative path, :attachmentField parameters, or async callbackNo prefixRoot configuration; prefixes converter folders
originalName, mimeTypeStringDerived from the sourceManager options only
metadataMetadata objectNo supplied metadataManager options only
maxBytesPositive integerNo size ceilingsources.maxBytes and manager options

meta requests extraction, whereas metadata supplies already known values. The media policy controls whether extraction is synchronous, deferred, or disabled. Outside Lucid, the application owns database persistence and post-commit job scheduling.

meta activates the default metadata extractors during persist(). With Lucid relations, preComputeUrl calculates the public URL when the relation is read and keeps it only in memory. With Lucid relations, variants schedules the listed keys after the blob and its link are committed. It is resolved with the same priority as the other persistence options.

Folder and rename defaults ​

folder accepts a relative path or an async callback. rename accepts true, false, or a callback that returns the stored filename. normalizeFileName defaults to true and makes names supplied by rename: false or a callback portable across storage backends. Defaults may use V5-style :attribute parameters, which are resolved only when a model context exists, such as a Lucid decorator or relation:

Set normalizeFileName: false only when the selected storage backend accepts the supplied filename unchanged.

ts
defaults: {
  folder: 'uploads/:name',
  rename: () => ':name-file.jpg',
}

String model attributes are lowercased, HTML-escaped, and slugified. A parameter that is unknown or not a string remains unchanged. See Creating attachments for callbacks and standalone behavior.

Variant folders ​

variant.basePath is the shared prefix for generated variants. Each converter's optional folder is a subfolder inside it. Both accept attachment parameters or an async callback receiving { attachment }. These fields describe the source attachment, not a Lucid owner: a table-backed blob may be shared by several owners. Their values are inserted verbatim.

ParameterAttachment value
:idThe attachment identifier.
:diskThe selected storage disk.
:nameThe stored filename, including its extension.
:originalNameThe original filename supplied by the client or source.
:pathThe complete stored path of the original.
:extnameThe original extension, without the leading dot.
:mimeTypeThe detected or supplied MIME type.
:blurhashThe blurhash, when it has been computed.

In particular, :name preserves the storage filename and its extension. This differs from defaults.folder: model parameters in that option are slugified, whereas variant parameters always refer to the source attachment and are not transformed.

ts
variant: { basePath: 'variants/:id' },
converters: {
  thumbnail: {
    resize: { width: 320 },
    format: 'webp',
    folder: 'images', // variants/<attachment id>/images/<generated file>
  },
}

Use a callback when the location needs the original path:

ts
import { dirname, join } from 'node:path/posix'

variant: {
  basePath: ({ attachment }) => join(dirname(attachment.path), 'variants', attachment.name),
}

Or use a callback to group every generated variant by the date at which it is generated:

ts
import { DateTime } from 'luxon'

variant: { basePath: 'variants' },
converters: {
  thumbnail: {
    resize: { width: 320 },
    format: 'webp',
    folder: () => DateTime.now().toFormat('yyyy/LL'),
  },
}

This writes the thumbnail to a path such as variants/2026/09/thumbnail.webp.

URLs ​

Storage URL methods depend on adapter capabilities. AdonisDriveStorage uses the selected Drive disk; LocalFileStorage requires baseUrl for public URLs. preComputeUrl resolves public URLs during relation reads and keeps them in memory.

See File URLs for all ways to obtain a URL: application routes, singular relations, collections, the service, signed URLs, precomputed .url, and legacy JSON.

Media binaries ​

Declare external media binaries once under media.binaries. The autodetected video, PDF, and office-document converters use these values. A command or timeout declared directly on a converter remains more specific and takes precedence.

ts
const binaries = {
  ffmpeg: { command: '/opt/media/bin/ffmpeg', timeout: 15_000 },
  ffprobe: { command: '/opt/media/bin/ffprobe', timeout: 5_000 },
  pdftoppm: { command: '/opt/media/bin/pdftoppm', timeout: 10_000 },
  pdfinfo: { command: '/opt/media/bin/pdfinfo', timeout: 5_000 },
  soffice: { command: '/opt/media/bin/libreoffice', timeout: 20_000 },
}

export default defineConfig({
  storage: LocalFileStorage.fromApp,
  media: {
    binaries,
  },
})

The default metadata extractors reuse the ffprobe and pdfinfo declarations automatically. Converter-level values remain more specific and take precedence over shared binary values.

Media metadata ​

Set meta: true on an attachment or in defaults to enable the built-in profile. It extracts the historical metadata shape: dimension, orientation, date, host, gps, duration, codecs, PDF pages, and PDF version. No media.metadata entry is required.

For the default image profile, install the optional dependencies:

sh
npm install sharp exifreader
SourceDefault extractionRequirement
Supported raster imagesSharp technical metadata, then EXIF/GPS where supportedsharp and exifreader
SVGSharp technical metadata, including dimensions; never EXIFsharp only
Audio/videoDuration, codecs, and video dimensionsffprobe executable
PDFDimensions, pages, version, and creation datepdfinfo executable
DOCX and other unsupported formatsNo extracted metadata; the original can still be storedNone for metadata

Sharp adds format, density, hasAlpha, and available page/frame information. EXIF enriches that result with fields such as orientation description, date, software, and GPS. Expanded ExifReader coordinates are normalized to gps.latitude, gps.longitude, and gps.altitude automatically; no application reader wrapper or type cast is needed.

Image readers are loaded only when a matching extraction runs, not when configuration is created. Missing required packages raise E_MISSING_PACKAGE; corrupt supported files and binary failures still raise errors. Unsupported formats are skipped rather than sent to an incompatible parser. In particular, meta: true does not imply that DOCX metadata is supported. The existing binary path configuration applies to ffprobe and pdfinfo unchanged.

Advanced: override the extractors ​

media.metadata completely replaces the default profile. Extractors receive the finalized attachment information and source bytes. Their results are merged in declaration order; metadata supplied by the application wins on key conflicts.

ts
import { defineConfig, LocalFileStorage } from '@jrmc/adonis-attachment'
import { createDefaultMetadataExtractors } from '@jrmc/adonis-attachment/media/metadata'

export default defineConfig({
  storage: LocalFileStorage.fromApp,
  media: {
    metadata: createDefaultMetadataExtractors({
      ffprobe: { command: '/opt/media/bin/ffprobe', timeout: 5_000 },
      pdfinfo: { command: '/opt/media/bin/pdfinfo', timeout: 5_000 },
    }),
  },
  defaults: { meta: true },
})

command accepts either an executable available on PATH or an absolute path. Set sharp, exif, ffprobe, or pdfinfo to false to disable an extractor. Use metadata: [] to disable all default extraction even when an attachment requests meta: true.

For example, createDefaultMetadataExtractors({ sharp: false }) keeps EXIF and the binary extractors but disables Sharp technical metadata, including SVG metadata. To customize Sharp, pass a metadata factory as sharp; the actual imported sharp function is accepted.

Performance policy ​

Metadata runs synchronously by default, preserving the v5 behavior: persist() waits for the extraction. Set command timeouts for external binaries to prevent a stalled process from holding a request indefinitely:

ts
media: {
  binaries: {
    ffprobe: { timeout: 5_000 },
    pdfinfo: { timeout: 5_000 },
  },
}

For video or PDF-heavy applications, defer extraction to the queue after the attachment row is committed. The Lucid store provides its metadata persister; another ORM must provide store.metadataPersister with a persistMetadata(attachment, metadata) method.

ts
media: {
  metadataPolicy: {
    mode: 'deferred',
    variants: false,
  },
}

variants defaults to true for v5 compatibility. Set it to false when thumbnails do not need their own metadata. This disables both synchronous extraction and deferred metadata jobs for variants, even with defaults.meta: true. An explicit meta: false on a generation request also overrides the global metadata default. Originals retain their own metadata settings. In deferred mode, configure the metadata processor shown in Background processing so workers handle extract-metadata jobs.

With custom persistence, call AttachmentService.scheduleMetadataExtraction(attachment) only after the record that owns the attachment has committed. This prevents a worker from updating a record that does not exist yet. See the complete custom-persistence example.

ts
import { defineConfig, LocalFileStorage, type MediaMetadataExtractor } from '@jrmc/adonis-attachment'

const imageMetadata: MediaMetadataExtractor = {
  supports: ({ attachment }) => attachment.mimeType.startsWith('image/'),
  async extract({ body }) {
    return { sourceBytes: body.byteLength }
  },
}

export default defineConfig({
  storage: LocalFileStorage.fromApp,
  media: { metadata: [imageMetadata] },
})

The default profile already includes Sharp. To explicitly replace the profile with only Sharp image metadata, use the optional adapter instead of writing the extractor yourself:

ts
import sharp from 'sharp'
import { createSharpMetadataExtractor } from '@jrmc/adonis-attachment/media/sharp'

media: { metadata: [createSharpMetadataExtractor(sharp)] }

For audio and video, createFfprobeMetadataExtractor() from @jrmc/adonis-attachment/media/binaries reads duration, codecs and video dimensions. It accepts the same command and timeout options when the v5 profile is not used.

Generated variants use the same extraction pipeline when their original was scheduled with meta: true. Converter-provided metadata still takes precedence over extracted values.

Source limits ​

sources configures the attachment manager. The package enforces no size policy by default - validate uploads in your app. You may set a technical byte ceiling:

ts
export default defineConfig({
  storage: LocalFileStorage.fromApp,
  sources: { maxBytes: 10 * 1024 * 1024 }, // 10 MB, per source
})

Lucid table names ​

By default the Lucid integration uses adonis_attachments (blobs) and adonis_attachment_links (links). The link table name is always derived from the blob table name with string.singular:

tableNamelink table
adonis_attachmentsadonis_attachment_links
media_attachmentsmedia_attachment_links
ts
export default defineConfig({
  storage: LocalFileStorage.fromApp,
  store: LucidAttachmentStore.configure({ tableName: 'media_attachments' }),
})

LucidAttachmentStore.fromApp uses the default names. configure() selects custom names and optionally enables legacy JSON processing. Use store: false when attachments should not use Lucid, even if Lucid is installed for the rest of the application.

WARNING

Use the same tableName when you generate the migration and at runtime - the models are pointed at these names at boot. Changing configuration does not rename existing tables. To keep an existing attachments and attachment_links schema, explicitly set store: LucidAttachmentStore.configure({ tableName: 'attachments' }).

Read route & background processing ​

  • store supplies the repository, metadata persistence, and variant persistence.
  • Declare router.attachments() in start/routes.ts to expose a read route. It allows only defaultDisk unless disks is specified. The former route config key has been removed. See Serving files.
  • queue controls where jobs run. Omit it for an in-memory queue, or use named connections and a typed default. Direct drivers, instances, and factories remain supported.
  • jobHandler is an advanced override for memory processing. It takes precedence over the store's default processor. External forwarding jobs can use createAttachmentProcessor(app).

Next: Creating attachments.