mike-kostov
PhxMediaLibrary - Spatie's Media Library, reimagined for Elixir/Phoenix
Hey, I recently picked up learning Elixir and Phoenix, and this is my first Open Source library, ever. I’ve 10+ years of web dev experience with PHP and Laravel, and I am so used to the human-friendly ecosystem that I tried to translate that same feeling into idiomatic Elixir. I’d love to hear your feedback on how I did!
I did use AI, conversing with it on structure and what would the “Elixir way” entail.
PhxMediaLibrary
Ecto-backed media management library for Elixir and Phoenix, with focus on Developer Experience (DX), inspired by Spatie’s Laravel Media Library.
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
collection :images, max_files: 20, max_size: 10_000_000
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 — One-Line 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
| Category | What you get |
|---|---|
| Schema integration | Polymorphic has_media() macro, declarative DSL for collections & conversions |
| Collections | MIME validation, file limits, size limits, single-file mode, fallback URLs |
| Image conversions | Thumbnails, resizes, format conversion, responsive srcset — optional, works without libvips |
| Metadata extraction | Auto-extract dimensions, EXIF, format, type classification; stored in metadata JSON field |
| Remote URLs | add_from_url/3 with scheme validation, custom headers, timeout, download telemetry |
| Storage | Local disk, S3, in-memory (tests), or custom adapters via PhxMediaLibrary.Storage behaviour |
| Streaming uploads | Files streamed to storage in 64 KB chunks — never loaded entirely into memory |
| Direct S3 uploads | presigned_upload_url/3 + complete_external_upload/4 for client-to-S3 without proxying |
| Soft deletes | Opt-in deleted_at with restore/1, purge_trashed/2, query scoping, and purge Mix task |
| Async processing | Task (default) or Oban adapter with persistence, retries, and process_sync/2 |
| LiveView | Drop-in <.media_upload> and <.media_gallery> components, LiveUpload helpers |
| Security | Content-based MIME detection (50+ formats via magic bytes), SHA-256 checksums |
| Batch ops | clear_collection/2, clear_media/1, reorder/3, move_to/2 |
| Telemetry | :start/:stop/:exception spans for add, delete, conversion, storage, batch, download |
| Errors | Tagged tuples + structured exceptions (Error, StorageError, ValidationError) |
| Queries | media_query/2 returns composable Ecto.Query |
| View helpers | <.media_img>, <.responsive_img>, <.picture> components |
| Mix tasks | Install, regenerate conversions, clean orphans, purge deleted, generate migrations |
Most Liked
zencilencer
Nice! Thank you for your contributions.. ![]()
1
Popular in Announcing
In short
Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
Hi there,
for my project DBLSQD, I needed a file storage solution that is a bit more flexible than Arc. Because I thought others might f...
New
I’d like to announce a small library called boundaries.
This is an experimental project which explores the idea of enforcing boundaries ...
New
PhoenixWS - Websockets over Phoenix Channels
Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention.
Assuming you have ...
New
Hello everyone,
I wrote a small library today called MapDiff.
It returns a map listing the (smallest amount of) changes to get from map...
New
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
Hey folks!
Want to present a toolkit for writing command-line user interfaces.
It provides a convenient interface for
colorizing text...
New
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
New
Hello all,
I have been working on my proposed project called Tensorflex as part of Google Summer of Code 2018.. Tensorflex can be used f...
New
Other popular topics
lets say i have a sample like
a = 20; b = 10;
if (a > b) do
{:ok, "a"}
end
if (a < b) do
{:ok, b}
end
if (a == b) do
{:ok, "equa...
New
Hi guys, i’m new in the Elixir world, and i have to say, that i love it!
i’m having some problem to understand anonymous functions with ...
New
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: )
Hello all, this is ...
New
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
i’m a new one to elixir
which editor can i use
vs code? or atom?
Thanks! :smiley:
New
Hi folks,
Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
New
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
Hi,
I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









