Library / SDK
elixir-waffle/waffle avatar
elixir-waffle/waffle

waffle gives Elixir apps a file upload layer with storage adapters

Flexible file upload and attachment library for Elixir

821 stars93 forksElixirLicense varies

At a glance

What is it?
Waffle is an Elixir library for file uploads and attachments, forked from Arc, with storage adapters including local disk and Amazon S3, an optional Ecto integration for changeset casting and versioned URLs, and a generator for uploader modules. It publishes no releases and its licence metadata is unclassified.
Who is it for?
Elixir teams that already use Ecto and want uploads to be part of their domain model should adopt waffle, because the definition module, the storage adapters and the changeset casting remove the plumbing around saving, transforming and versioning files.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 33 days ago.
What is it written in?
Mainly Elixir, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Uploads modelled as a definition module, not a controller

Waffle is a flexible file upload library for Elixir with straightforward integrations for Amazon S3 and ImageMagick, and it is aimed at a specific set of problems.

The README states them in one line: you want to apply file transformations, manage access rules, save files, or process files asynchronously. Each of those is a reason a hand rolled upload endpoint gets messy, and the library's answer is to move the upload logic out of the request handler and into a module you define.

That module is the core of the library. The generator mix waffle.g avatar creates it, and it is a module that uses Waffle.Definition and, if you use Ecto, Waffle.Ecto.Definition. What it holds is a storage_dir callback, which decides where a given version of a file for a given record lives. That is a small surface, and it is where the interesting decisions go: the path expression in the example depends on the user id, which means the layout of your stored files is derived from your domain model rather than from a controller action.

The library has a notable lineage. The README states that it was forked from Arc at version v0.11.0, with thanks to Sean Stavropoulos for creating Arc. So this is not a new design from scratch; it is a maintained continuation with its own direction, and the Arc name in your dependency history is worth knowing about if you are looking for prior art or older fixes.

It is published on Hex as waffle, with documentation on HexDocs, and the maintainer invites organisations already using it to get in touch.

The definition module and the storage directory callback

The uploader module looks like this in the README, and it is short enough to reproduce in full:

elixir
defmodule MyApp.Avatar do
  use Waffle.Definition
  use Waffle.Ecto.Definition

  def storage_dir(_version, {_file, user}) do
    "users/#{user.id}/avatar/"
  end
end

Three things are happening in that snippet. The module opts into the Waffle definition behaviour, which is what gives the library its knowledge of how to store, transform and version the files. It opts into the Ecto definition behaviour as well, which is what ties a stored file to a database record. And it implements storage_dir with two arguments, a version and a tuple of the file and the record.

That tuple is the interesting part. The callback receives the record the file belongs to, so the path can depend on the record rather than on a fixed convention. In the example the path is built from user.id, so every user gets their own directory and the layout cannot drift out of sync with the data model.

The version argument is what makes versioning possible, and the README mentions versioned URLs for cache busting as part of the Ecto integration. A versioned URL changes when the file changes, so a browser or a CDN does not serve a stale image after an avatar is replaced. The leading marker on the version parameter in the example means the callback ignores it and returns the same path for every version, which is a deliberate choice worth making deliberately.

The definition module is also where you would add transformations and access rules, since those are per-entity behaviours in this design rather than global configuration.

Ecto integration: a schema field and a changeset cast

The Ecto side of the library is packaged separately as waffle_ecto, and the README treats it as an optional companion rather than part of the core dependency.

The schema change is one field:

elixir
schema "users" do
  field :avatar, Avatar.Type
end

Avatar.Type is a type generated from the definition module, so the schema refers to the same module that decides where the file is stored. The field type is what gives the changeset machinery something to cast into.

The upload itself is cast like any other attribute, which is the part that makes Waffle pleasant in an Ecto application:

elixir
changeset
|> cast_attachments(
  params,
  [:avatar],
  allow_paths: true,
  allow_urls: true
)

cast_attachments takes the list of attachment fields, here the avatar, and casts the incoming parameters into them. The two options deserve attention because they widen what the server will accept. allow_paths: true lets a payload reference a local file path, and allow_urls: true lets it reference a remote URL. Both are conveniences for a trusted client and both are risks in a multi-tenant application, where a user who can control the params may be able to point the server at a local path or at an address of their choosing. If your upload form only ever receives files from a browser, leaving both options off is the safer default.

The README notes that WaffleEcto supports changeset integration and versioned URLs for cache busting, and points to the separate waffle-ecto documentation site for the details.

Storage adapters decide where bytes land

Files do not go anywhere until you say where, and the README shows that as a single configuration key:

elixir
config :waffle, storage: Waffle.Storage.Local

That sets the default storage adapter to the local filesystem driver. The README points to the storage configuration documentation for the full list of supported adapters, and names Amazon S3 among them, with separate example documents for the local storage driver and the S3 storage driver.

The distinction between the two layers is worth being clear about, because it is easy to conflate them. The definition module decides the logical location of a file for a given record and version. The storage adapter decides the physical mechanism that puts it there. A definition that produces users/42/avatar/ will be handled by the local adapter as a directory on disk and by the S3 adapter as a key prefix in a bucket.

That split is what makes the S3 integration straightforward rather than a rewrite. Moving a deployment from local disk to S3 is a configuration change in most applications, with the definition module untouched, provided you have not hard-coded assumptions about the filesystem elsewhere in your code.

Transformations sit alongside storage. The library advertises straightforward integrations for Amazon S3 and ImageMagick, and ImageMagick appears in the project's own development container, which is a good indication of how the transformation path is exercised in testing.

The Docker setup shows the intended test workflow

The repository ships a Dockerfile and a docker-compose.yml, and together they describe how the project expects to be developed and tested.

The image is built on the elixir:1.15-alpine base and adds ImageMagick, with the comment explaining that the convert utility is what performs image transformations. It then installs the Hex and rebar tooling locally, copies the project into a working directory, sets MIX_ENV to test and runs mix deps.get and mix compile before the command mix test.

dockerfile
FROM elixir:1.15-alpine

# add `convert` utility from imagemagick for image transformations
RUN apk add --no-cache imagemagick

The compose file builds that image from the repository, keeps the container alive with a tail command, loads environment variables from an .env file and mounts the working directory into the container:

yaml
services:
  waffle:
    build:
      context: .
      dockerfile: Dockerfile
    command: tail -f /dev/null
    env_file:
      - .env
    volumes:
      - .:/srv/app:cached

The environment file matters, because the S3 adapter needs credentials and the example repository carries an example.env to copy from. Mounting the source rather than baking it in is what makes the container usable for development, since the test suite runs against the working tree.

Two observations. The base image pins Elixir 1.15 on Alpine, which is a conservative choice for a container that needs native image tooling rather than the newest runtime. And the compose file declares version 2.4, a field that recent compose versions warn about, so expect a notice when you run it.

Where Waffle is the wrong tool

The boundaries are clear enough to state directly.

It is Elixir only. There is no port path here; a team on another stack has to reimplement the same ideas. Within Elixir it is also an Ecto-shaped tool, and the Ecto integration lives in a separate package with its own version line, so you are adopting two dependencies that must be kept compatible.

The release story is the weakest point. The repository publishes no GitHub releases, and the version constraint the README gives is waffle at about 2.0 with waffle_ecto at about 0.0. A package still sitting at 0.0 in its second package is a signal that its interface may still move, and with no tagged artefacts to diff, an upgrade is something you discover by running the tests rather than by reading a changelog entry. A CHANGELOG.md does exist in the repository, which helps, but it is not the same as a release history.

The security surface is the other thing to think about before you wire it into a public form. allow_paths and allow_urls decide whether the server will read from local paths and from remote URLs on the caller's say-so, and the versioned URL mechanism changes the URL whenever the stored file changes. Neither feature is documented with a security model in the README, so the responsibility for restricting them sits with the application.

And the transformation story depends on external binaries. ImageMagick is not optional in practice if you use that path, which is why the project's own container installs it, and it means your deployment image carries that dependency too.

Licence, maintenance state and what to check before depending on it

The licence deserves a note because the metadata and the file disagree in a useful way. The package licence field is recorded as unknown, and the README carries the full Apache License, Version 2.0 text with two copyright lines, one for Boris Kuznetsov in 2019 and one for Sean Stavropoulos in 2015. Both authors are therefore covered by the terms in the README, which is the text that governs in a legal review, whatever a package index displays.

The maintenance picture is one of continued commits without formal releases. The repository is not archived and the last push was on 2026-09-20. There is a CHANGELOG.md at the root and an ISSUE_TEMPLATE.md, and the presence of a Dockerfile, a docker-compose.yml and a config directory suggests an actively worked-on project rather than an abandoned fork. The absence of releases is the notable gap, and it is why pinning the Hex version in mix.lock matters.

Two checks are worth doing before you depend on this library. First, read the storage configuration documentation for your chosen adapter, since the local and S3 drivers will not necessarily take the same configuration keys, and the S3 path will need credentials that the example.env file hints at. Second, decide deliberately whether you need the Ecto integration at all. The core library gives you the definition module and the storage adapters, and if your application does not use Ecto you can skip waffle_ecto entirely, which removes the 0.0 versioned package from the equation and keeps your dependency set smaller.

The library also keeps example documents for the local and S3 drivers under documentation, which are the fastest way to see a complete configuration before writing your own.

Editorial conclusion

Elixir teams that already use Ecto and want uploads to be part of their domain model should adopt waffle, because the definition module, the storage adapters and the changeset casting remove the plumbing around saving, transforming and versioning files. It is the wrong choice for a service that is not Elixir, and it is the wrong choice for a team that needs a supported release trail, since the repository publishes no GitHub releases and the package licence metadata is unclassified. Before adopting, read the licence text in the README rather than the package metadata, check the storage configuration key against the adapter list, and pin the waffle and waffle_ecto versions together since they are versioned independently.

Frequently asked questions

How do I add Waffle to an Elixir project?

Add the :waffle and :waffle_ecto dependencies to mix.exs, with the README showing {:waffle, "~> 2.0"} and {:waffle_ecto, "~> 0.0"}, then configure the storage adapter with config :waffle, storage: Waffle.Storage.Local or the adapter you want.

What does the Waffle definition module do?

It is where you decide how files are stored and transformed. The README example creates it with mix waffle.g avatar, uses Waffle.Definition and Waffle.Ecto.Definition, and implements a storage_dir callback that receives the version and the file and record tuple, so the path can depend on the record.

How are files attached to an Ecto changeset?

The README casts the payload with cast_attachments, passing the list of attachment fields such as [:avatar]. The options allow_paths and allow_urls control whether the server may read from local paths and from remote URLs, so leave them off unless your clients require it.

Which storage backends does Waffle support?

The README describes straightforward integrations for Amazon S3 and ImageMagick, and the storage configuration documentation lists the supported adapters including S3. The library also ships separate example documents for the local storage driver and the S3 storage driver.

Official sources

  1. elixir-waffle/waffle on GitHub
  2. Issues
  3. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elixir-waffle-waffle.svg)](https://hysenlabs.com/projects/elixir-waffle-waffle)

Was this useful?