PhxMediaLibrary

Hex.pmHex DocsLicense

An Ecto-backed media library for Elixir and Phoenix, inspired by Spatie's Laravel Media Library.

Associate files with any Ecto schema using a fluent, composable API. You get collections, image conversions, LiveView components, and several storage backends.

Quick look

defmodule MyApp.Post do
use Ecto.Schema
use PhxMediaLibrary.HasMedia
schema "posts" do
field :title, :string
has_media() # all media for this model
has_media(:images) # scoped to "images" collection
has_media(:avatar) # scoped to "avatar" collection
timestamps()
end
media_collections do
# webp: true serves WebP (keeps the original); responsive: true adds srcset variants
collection :images, max_files: 20, max_size: 10_000_000, webp: true, responsive: true
collection :documents, accepts: ~w(application/pdf text/plain)
collection :avatar, single_file: true, fallback_url: "/images/default.png"
end
media_conversions do
convert :thumb, width: 150, height: 150, fit: :cover
convert :preview, width: 800, quality: 85
convert :banner, width: 1200, height: 400, fit: :crop, collections: [:images]
end
end
# Add media with a fluent pipeline
{:ok, media} =
post
|> PhxMediaLibrary.add("/path/to/photo.jpg")
|> PhxMediaLibrary.using_filename("hero.jpg")
|> PhxMediaLibrary.with_custom_properties(%{"alt" => "Hero image"})
|> PhxMediaLibrary.to_collection(:images)
# Retrieve
PhxMediaLibrary.get_media(post, :images)
PhxMediaLibrary.get_first_media_url(post, :images, :thumb)
# Delete
PhxMediaLibrary.delete(media)
{:ok, count} = PhxMediaLibrary.clear_collection(post, :images)

LiveView uploads

defmodule MyAppWeb.PostLive.Edit do
use MyAppWeb, :live_view
use PhxMediaLibrary.LiveUpload
def mount(%{"id" => id}, _session, socket) do
post = Posts.get_post!(id)
{:ok,
socket
|> assign(:post, post)
|> allow_media_upload(:images, model: post, collection: :images)
|> stream_existing_media(:media, post, :images)}
end
def handle_event("save_media", _params, socket) do
{:ok, media_items} = consume_media(socket, :images, socket.assigns.post, :images)
{:noreply, stream_media_items(socket, :media, media_items)}
end
end
<form phx-change="validate" phx-submit="save_media">
<.media_upload upload={@uploads.images} id="post-images" />
<button type="submit">Upload</button>
</form>
<.media_gallery media={@streams.media} id="gallery">
<:item :let={{_id, media}}>
<.media_img media={media} conversion={:thumb} class="rounded-lg" />
</:item>
</.media_gallery>

Features

CategoryWhat you get
Schema integrationPolymorphic has_media() macro, declarative DSL for collections & conversions
CollectionsMIME validation, file limits, size limits, single-file mode, fallback URLs
Image conversionsThumbnails, resizes, format conversion, responsive srcset. Optional, works without libvips
WebP & HEICPer-collection webp: true transcodes raster/HEIC uploads to WebP and serves them (original kept); responsive variants inherit WebP
Metadata extractionAuto-extract dimensions, EXIF, format, type classification; stored in metadata JSON field
Remote URLsadd_from_url/3 with scheme validation, custom headers, timeout, download telemetry
StorageLocal disk, S3, in-memory (tests), or custom adapters via PhxMediaLibrary.Storage behaviour
Streaming uploadsFiles streamed to storage in 64 KB chunks, never loaded entirely into memory
Direct S3 uploadspresigned_upload_url/3 + complete_external_upload/4 for client-to-S3 without proxying
Soft deletesOpt-in deleted_at with restore/1, purge_trashed/2, query scoping, and purge Mix task
Async processingTask (default) or Oban adapter with persistence, retries, and process_sync/2
LiveViewDrop-in <.media_upload> and <.media_gallery> components, LiveUpload helpers
SecurityContent-based MIME detection (50+ formats via magic bytes), SHA-256 checksums
Batch opsclear_collection/2, clear_media/1, reorder/3, move_to/2
Telemetry:start/:stop/:exception spans for add, delete, conversion, storage, batch, download
ErrorsTagged tuples + structured exceptions (Error, StorageError, ValidationError)
Queriesmedia_query/2 returns composable Ecto.Query
View helpers<.media_img>, <.responsive_img>, <.picture> components
Mix tasksInstall, regenerate conversions / WebP / responsive variants, clean orphans, purge deleted, generate migrations

Installation

def deps do
[
{:phx_media_library, "~> 0.8"},
# Optional: Image processing (requires libvips)
{:image, "~> 0.54"},
# Optional: S3 storage
{:ex_aws, "~> 2.5"},
{:ex_aws_s3, "~> 2.5"},
{:sweet_xml, "~> 0.7"},
# Optional: Async processing with Oban
{:oban, "~> 2.18"}
]
end
# config/config.exs
config :phx_media_library,
repo: MyApp.Repo,
default_disk: :local,
# Choose the column type that matches your models' primary key type.
# :binary_id UUID (default, keeps existing installs working)
# :integer bigint (best for integer PK apps)
# :string varchar (works with any PK type; recommended for new apps)
mediable_id_type: :string,
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Disk,
root: "priv/static/uploads",
base_url: "/uploads"
]
]
# For integer PK apps, pass --id-type to generate the right migration column:
mix phx_media_library.install --id-type integer
mix ecto.migrate

mediable_id_type sets the database column type for the polymorphic foreign key. The default is :binary_id (UUID) for backwards compatibility. :string works with any primary key type and is the best choice for new apps. Set it explicitly today and you won't need to change anything if the default shifts in a future release.

Note: The :image dependency is optional. PhxMediaLibrary works for file storage without it. Image conversions require :image to be installed.

Guides

Detailed documentation is organized into focused guides:

GuideCovers
Getting StartedInstallation, configuration, schema setup, adding & retrieving media
Collections & ConversionsValidation rules, image processing, responsive images, checksums
LiveView IntegrationUpload & gallery components, LiveUpload helpers, event notifications, view helpers
StorageLocal disk, S3, in-memory, custom adapters, path conventions
Error HandlingTagged tuples, custom exceptions, MIME detection
TelemetryEvents reference, attaching handlers, metrics examples
Advanced UsageReordering, mix tasks, testing strategies

Full API documentation is available on HexDocs.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork it
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -am 'Add my feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Create a Pull Request

License

This project is licensed under the MIT License. See the LICENSE file for details.

Acknowledgments

Inspired by Spatie's Laravel Media Library, bringing its excellent developer experience to the Elixir ecosystem.