Elixir Composer
Composer is an app generation tool for Elixir.
Elixir package maintainers ship generators and igniter installers. These tools are indispensable, yet turning a generated skeleton into a working prototype can still be a lot of work.
Composer builds on generators and igniter to produce a skeleton that is better fit for purpose from the start, so you reach a first working prototype sooner. Composer manages changes like:
- removing generator defaults you don't want
- injecting layouts and styles
- adding pages and navigation
- creating seed data
- configuring add-on packages: auth, mailers, API extensions, and more
Composer is inspired by the installer feature on
AshHQ. It extends that idea so that cookbooks
are easy to:
- bundle with any Elixir framework (eg Phoenix, Ash, Jido, Mob, TermUI, Astral...)
- write and tweak, for every developer (not just package maintainers)
- share between teams
- search and discover, by humans and agents
Overview
Concepts
With composer, a cookbook contains recipes; recipes are lists of steps, organized in groups; steps make changes and can render templates.
- A step is an atomic/composable Igniter task. For search and discovery,
steps also contain metadata: a summary, facets such as
action: :removeorframeworks: [:phoenix], tags, and what itrequires. Addressed as"cookbook:step". - A recipe is a named, ordered list of steps, with a shortdoc, optional
doc and flags. Recipes live in groups, one
.exsfile per group, and are addressed as"cookbook:group/name". - A cookbook is a self-contained collection of steps (with their
templates) and recipes, shipped by a framework, a standalone package, or your own app.
Composer ships a built-in
corecookbook; your personal recipes live in thelocalcookbook (~/.config/composer/recipes/).
Mix tasks
| Task | Purpose |
|---|---|
composer.new |
Make your app a cookbook and generate its recipe |
composer.recipe.{list,show,run,edit} |
List, inspect, run and edit recipes |
composer.step.{list,find,help,run,edit} |
List, search, inspect, run and edit steps |
composer.cookbook.list |
List discovered cookbooks |
composer.gen.{step,recipe,cookbook} |
Scaffold a step, recipe or cookbook |
composer.cookbook.{manifest,lint} |
Write a cookbook's manifest; lint it |
composer.completion, composer.index |
Shell completion |
composer.lsp |
Language server for recipe files |
Recipes
A recipe is plain data. mix composer.new generates one; edit it to taste:
# lib/composer/my_app/recipes/app.exs
[
default: [
shortdoc: "My app's setup",
flags: [oban: "Add Oban"],
steps: [
"core:phx/remove_daisy_ui", # undo a phx.new default
"core:phx/remove_topbar",
"core:setup/gitignore", # project hygiene
"core:setup/credo", # add a package
{"composer_lab:ash/postgres", argv: ["--all"]}, # a step with arguments
"hex:ash", # a Hex package's own installer
{"hex:oban", if: :oban}, # optional, behind --oban
"my_app:deploy_config", # your own step
{:use, "local:dev/tools"} # include another recipe
]
]
]
It runs as one diff, with the flags it declares:
$ mix composer.recipe.run app/default --oban
Getting started
1. Create an app and add the dependency
mix igniter.new my_app --with phx.new
cd my_app
Add composer to your deps in mix.exs:
def deps do
[
{:composer, "~> 0.2.0", only: :dev},
# ...
]
end
mix deps.get
2. Generate your recipe
mix composer.new
This makes your app a cookbook and writes
lib/composer/my_app/recipes/app.exs, holding the recipe
app/default: every built-in step in a sensible order, each with a one-line
description. Open it (mix composer.recipe.edit app/default) and make it
yours:
- Delete any step you don't want — the file is yours.
- Reorder steps freely; they run top to bottom.
- Tag a step with
if: :flagto make it optional — it then only runs when you pass the matching--flag. Describe the flag underflags:.
3. Run it
mix composer.recipe.show app/default # preview: docs, flags, resolved steps, warnings
mix composer.recipe.run app/default
Nothing is applied blindly. Every reference is resolved and every step's
requires checked before anything runs. The run prints a plan showing what
each step resolved to, then applies all changes — your steps and package
installers together — as one diff, recorded as one git commit (a dirty work
tree is refused). Pass --dry-run to preview the diff, --no-commit to skip
the commit.
Only dependencies come first: packages that ship an installer are added to
mix.exs and fetched up front, since their installers must be on disk to
run. When the run finishes, set up the database and go:
mix ecto.setup
mix phx.server
Note: the generated recipe enables the
vectorPostgres extension via thepg/extensionsandpg/vectorsteps. If your local Postgres doesn't have pgvector installed, either install it or delete those two steps from your recipe.
Keep the recipe in your repo: it documents exactly how your app was set up, and you'll copy it into your next project.
Running single steps
Every step also works on its own, no recipe required:
mix composer.step.run core:setup/credo,core:setup/quokka
mix composer.step.run phx/remove_daisy_ui # unqualified names work too
mix composer.step.run hex:oban # a package's own installer
mix composer.step.run hex:ash@github:ash-project/ash # ... from GitHub
mix composer.step.run hex:my_lib@path:../my_lib # ... from a local path
mix composer.step.run composer_lab:foo -- --bar # arguments after -- go to the step
mix composer.step.run hex:ash_authentication -- --auth-strategy password # ... or the installer
Finding steps
mix composer.cookbook.list # cookbooks, with step and recipe counts
mix composer.step.list --action remove # also --cookbook, --framework, --tag
mix composer.step.find tailwind
mix composer.step.show core:phx/bun # facets, requires, and a dry run
mix composer.step.edit core:phx/bun # open its source in your editor
mix composer.recipe.list
Every listing task takes --json, for scripts and agents.
Scripts and CI
Runs are interactive by default (diff, then confirm), except
composer.recipe.run, which always applies unattended. In a script or CI,
pass Igniter's standard --yes flag to auto-accept:
mix composer.new --yes && mix composer.recipe.run default --no-commit
Why not just…
mix igniter.new --install a,b,c? Great for installing packages that ship installers, and Composer runs those same installers rather than competing with them. Composer is for everything around that: removingphx.newdefaults (nothing else does this), packages that don't ship installers, ordering steps, optional flags, and keeping the whole ritual versioned in your repo.- A boilerplate/template repo? Templates fork away from
phx.newand rot. A recipe replays your preferences on top of whateverphx.newcurrently generates.
References and Hex packages
An unqualified name is looked up in the referring recipe's own cookbook
first, then in every cookbook. If it matches nothing, it is a Hex package:
"oban" means "hex:oban" — add the package and run its own installer.
Which of the two applies is upstream's business, and each run prints a plan
telling you which it was:
Composer plan
core:setup/credo
hex:oban — oban.install
hex:nimble_options — dependency only, ships no installer
core:setup/sort_deps
A name that matches steps in two cookbooks is an error that lists both, and
a name that looks like a typo of a step is reported rather than looked up on
Hex. Qualify the reference ("core:setup/credo") to be exact.
A hex: entry can pass arguments to the installer, set dependency options,
and install from a version, GitHub, git or a local path:
{"hex:ash_authentication", argv: ["--auth-strategy", "password"]},
{"hex:mix_test_watch", only: [:dev, :test], runtime: false},
{"hex:live_debugger", github: "software-mansion/live-debugger", only: :dev},
{"hex:my_lib", path: "../my_lib"},
"hex:ash@3.5" # the @ shorthand, as in mix igniter.install
All packages in a run still share one deps fetch and compile, and the plan
shows each one's source and arguments
(hex:my_lib (path ../my_lib)). See
Hex packages.
Composer deliberately ships no step for packages that provide their own
Igniter installer — oban, tidewave, ash and friends run upstream's
installer, so upstream stays the authority. Built-in steps exist only where
upstream ships nothing, and each does the minimum wiring the package's
installer would do if it existed.
Built-in steps
All built-in steps live in the core cookbook, grouped by directory
(phx/, setup/, ecto/, pg/, cluster/, tidewave/): reference them
as "core:<id>", e.g. "core:setup/credo". mix composer.step.list --cookbook core lists them with their facets.
| Id | Action | What it does |
|---|---|---|
phx/bun |
add | Replaces esbuild/tailwind with Bun: dep, package.json, config, watchers, aliases, tsconfig |
phx/minimal_app_layout |
patch | Replaces Layouts.app/1 with a minimal header/main layout |
phx/minimal_home_page |
patch | Replaces the phx.new marketing page with a minimal home page |
phx/remixicons |
add | Remix Icons as a Tailwind plugin with remix-* classes and a CoreComponents icon clause |
phx/remove_agents_md |
remove | Removes the generated AGENTS.md |
phx/remove_daisy_ui |
remove | Removes the :daisyui Mix dependency and CSS plugin blocks |
phx/remove_live_title_suffix |
remove | Removes the " · Phoenix Framework" title suffix |
phx/remove_theme_toggle |
remove | Removes the theme scripts, component, and usage |
phx/remove_topbar |
remove | Removes the topbar progress indicator |
phx/tailwind_formatter |
gen | HEEx formatter that sorts Tailwind classes |
setup/credo |
add | Dev/test dependency for static analysis |
setup/dotenv_parser |
add | Loads .env in runtime config; creates and gitignores .env |
setup/exsync |
add | Auto-recompilation on file changes (dev) |
setup/generator_defaults |
config | Generators default to binary_id + utc_datetime_usec |
setup/gitignore |
config | Adds macOS system files to .gitignore |
setup/mix_env_config |
config | config :app, env: Mix.env() for runtime environment checks |
setup/mix_test_watch |
add | Runs tests on file changes |
setup/quokka |
add | Credo-configured formatter plugin |
setup/sort_deps |
patch | Sorts mix.exs dependencies alphabetically |
ecto/base_schema |
gen | MyApp.Schema module with sensible key/timestamp defaults |
ecto/force_drop |
config | ecto.drop --force-drop in the mix alias |
ecto/uuidv7 |
add | Time-sortable UUID primary keys; updates your Schema module when present |
pg/extensions |
gen | Migration enabling citext, pg_trgm, unaccent, and vector |
pg/vector |
add | Vector search: dep, Postgrex types, config, extension migration (merges into existing extensions migration) |
cluster/gigalixir |
gen | Gigalixir deploy: buildpacks, Procfile, release migration scripts, SSL config |
cluster/gigalixir/libcluster |
config | Kubernetes clustering strategy for Gigalixir |
cluster/libcluster |
add | Node clustering: dep, supervision child, Gossip topology in dev |
tidewave/tidewave_config |
add | Creates .mcp.json pointing at a local Tidewave MCP server (follows hex:tidewave) |
oban_pro |
add | Oban Pro 1.8: Pro engine, Cron/Lifeline/Pruner/Queues services, migration (requires a license and an Oban 2.24+ setup; follows hex:oban) |
Steps that pattern-match against phx.new output warn loudly when they find
nothing to change, instead of silently no-oping.
Writing your own steps
Your app is a cookbook too, so a one-off step lives next to your recipe:
mix composer.gen.step deploy_config
writes lib/composer/my_app/steps/deploy_config.ex, with every facet
stubbed out, and a test:
defmodule Composer.MyApp.Steps.DeployConfig do
use Composer.Step,
summary: "Writes the deploy config",
action: :gen,
capability: [:deploy],
stability: :stable
@impl Igniter.Mix.Task
def igniter(igniter) do
Igniter.create_new_file(igniter, "config/deploy.exs", "# ...")
end
end
Reference it as "my_app:deploy_config" (or just "deploy_config" from your
own recipes). Composer.Templates, Composer.Helpers and
Composer.Versions provide conveniences for common patterns: rendering
template files, app/repo module names, checked file edits, migration
timestamps and latest-version lookups.
Sharing steps and recipes
Steps and recipes travel in cookbook packages: a Hex package with a
lib/composer/<package>/ tree. Add one as a dev dependency and its steps
and recipes appear in every listing task, referenced as
"<package>:<path>". Scaffold one with:
mix composer.gen.cookbook
mix composer.gen.step ash/postgres
mix composer.gen.recipe lv/base
mix composer.cookbook.manifest # commit the manifest
mix composer.cookbook.lint # in CI (--format github for annotations)
See the cookbooks guide for the layout, naming rules and bundling a cookbook with a framework.
Your personal recipes live outside any project, in the local cookbook:
mix composer.gen.recipe dev/tools --local # adds tools to ~/.config/composer/recipes/dev.exs
mix composer.recipe.list local # every local recipe, with its shortdoc
mix composer.recipe.run local:dev/tools
mix composer.new --from local:phx/app # start a new app from one
mix composer.cookbook.lint --local # check them
Lint reports every problem at its place in the file, and speaks the
formats editors and CI read (--format compact|json|github, --file for
on-save checks in Vim, Neovim, VS Code and others); see the
lint guide.
For editing recipes, mix composer.lsp runs composer-ls, a language
server beside your Elixir one: warm diagnostics on save (also in the files
that include the saved recipe), hover, goto-definition, completion of step
and recipe ids, and quick fixes. It needs the optional gen_lsp
dependency; see the Composer LSP guide.
A step entry can carry options: {"composer_lab:x", argv: ["--all"]} passes
arguments to the step, as -- does on mix composer.step.run, and combines
with if: ({"composer_lab:x", argv: ["--all"], if: :x}). On a hex:
entry, argv: goes to the package's installer.
Recipes compose: {:use, "local:dev/tools"} includes another recipe's
steps in place, to any depth, optionally behind a flag ({:use, "deploy/gigalixir", if: :gigalixir}); {:use, "base"} names a recipe in
the same group. At run time the tree is flattened into one ordered step
list — packages from the whole tree are deduped and fetched up front, a
single diff covers the entire run, and flags declared anywhere in the tree
surface on the task you invoke (any other --flag is rejected). See the
recipes guide.
Steps apply in list order, package installers included, so a step that patches what an installer wrote just goes after it:
"hex:oban",
"my_team:oban_tweaks"
{:queue, "some.task", []} is there for work that genuinely has to run
against the applied project on disk, after everything else.
Shell completion
mix composer.completion bash > ~/.local/share/bash-completion/completions/mix-composer
Completion of step and recipe ids reads a plain-text index under
_build/composer/, so it never starts the BEAM. Every composer task keeps
the index fresh; mix composer.index rebuilds it.
Compatibility
Composer supports only the latest stable Phoenix (currently 1.8) and its
phx.new output. Runs warn when applied to an app on an older Phoenix. CI
runs the generated recipe against a freshly generated phx.new app on
every commit, so generator drift breaks loudly here instead of silently in
your project.
Thanks
Composer stands on the work of the Igniter, Sourceror and ExAst contributors, and of the wider Elixir ecosystem.
License
MIT — see LICENSE.md.