Storehouse

Storehouse — Files for Phoenix

Files for Phoenix: uploads checked by their bytes, stored under safe keys, and served safely.

Photos people upload, a document's attachments, the exports your app makes. Storehouse checks each file by its contents, not its name, stores it on the local disk or in S3 (or R2, MinIO, Tigris), and serves it so it can never run a script on your app's origin.

Early release. Before 1.0, a minor version may change the API.

Install

You need Elixir 1.17+. Storehouse.Live also needs Phoenix LiveView 1.1+; the rest works in any Plug app.

Add Storehouse to the deps in mix.exs:

{:storehouse, "~> 0.2"}

On Elixir 1.17, add {:jason, "~> 1.4"} too. 1.18 and later have their own JSON.

Your first file

Tell Storehouse where to keep files, and serve them from your endpoint:

# config/runtime.exs
config :storehouse, Storehouse.Local, root: "/var/lib/my_app/uploads"

# lib/my_app_web/endpoint.ex, before the router
plug Storehouse.Local.Plug

Store a file:

{:ok, file} = Storehouse.store("priv/seeds/palette.png")

file
#=> %Storehouse.File{
#     key: "2026/10/0b6…-palette.png",
#     url: "/uploads/2026/10/0b6…-palette.png",
#     name: "palette.png",
#     content_type: "image/png",
#     bytes: 48_211,
#     width: 1600,
#     height: 1000,
#     preview: nil
#   }

Keep it in a record:

schema "users" do
  field :avatar, Storehouse.Ecto.File
end

user
|> User.changeset(attrs)
|> Storehouse.Ecto.put_file(:avatar, upload, accept: Storehouse.Check.images(), max_bytes: 5_000_000)
|> then(&Storehouse.Ecto.update(Repo, &1))

A list field ({:array, Storehouse.Ecto.File}) holds many, with add_files/4 and remove_file/3. The Ecto guide has both, and how each Active Storage call maps to Storehouse.

Or store the files of a LiveView upload:

{socket, results} = Storehouse.Live.consume(socket, :photos, accept: Storehouse.Check.images())

The Stores guide sets up S3, development and tests.

What you get

The full list is in Features.

Guides

Stores · Serving · LiveView · Direct uploads · Uploads from any client · Private files · Ecto · After the upload · Variants · Work out of the request · Features

How Storehouse compares

The Elixir libraries that keep an app's files, as their documentation and code describe them in October 2026. A correction is welcome as an issue.

Storehouse 0.2 Waffle 2.0 Capsule 1.0 phx_media_library 0.10 attached 0.2
Type checked from the bytes Yes No (extension) No Partly Partly
Safe serving headers Yes No No No No
CDN cache headers and purges Yes No No Partly No
Private files through the app Yes No No Yes No
Uploads straight to the store Yes No No Yes Partly
Streamed uploads, no temporary file Yes No No No No
Temporary until saved Yes No Partly Partly No
Checksums Yes No No Yes Yes
Files from a URL Yes Yes No No No
LiveView uploads Yes No No Partly No
Reading files back Yes Yes Yes Yes Yes
Your own metadata Yes No Yes Yes Yes
Photo metadata edited without re-encoding Yes No No No No
Several stores at once Yes Per uploader Per file Yes No
Background processing Yes No No Yes Yes
Variants Yes At upload No Yes On demand
Placeholders Yes No No Yes No
A table of files Optional No No Yes Yes
Duplicate uploads detected Yes No No No No
JavaScript client, non-LiveView endpoints Yes No No Partly No
Resumable and multipart uploads Yes No No No No
In-memory store for tests Yes No Yes Yes No
Dependencies Plug, Req, Telemetry Req, ImageMagick None Several Oban, Vix

Roadmap

Development

mix deps.get
mix test
mix credo --strict
mix dialyzer
node --test test/js/*.test.mjs

License

MIT. See LICENSE.