Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
Use this skill before implementing ANY file upload functionality.
auto_upload: true) for form submission patternsstatic_paths() — files won't be accessible without thiserror_to_string/1 output in templatesstatic_paths() — changes don't apply until restartFollow these steps in order:
uploads to static_paths() — see Static Paths Configuration belowstatic_paths() changes require a full restart to take effectallow_upload/3 in mount/3 — configure accepted types, entry limits, and file sizehandle_event("validate", ...) — required to trigger change trackinghandle_event("save", ...) — consume entries, create directories, copy filesallow_upload(:upload_name,
accept: ~w(.jpg .jpeg .png .pdf),
max_entries: 10,
max_file_size: 10_000_000
)@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:uploaded_files, [])
|> allow_upload(:photos,
accept: ~w(.jpg .jpeg .png),
max_entries: 5,
max_file_size: 10_000_000
)
{:ok, socket}
end
@impl true
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
@impl true
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :photos, fn %{path: path}, entry ->
dest = Path.join(["priv", "static", "uploads", safe_filename(entry.client_name)])
File.mkdir_p!(Path.dirname(dest))
File.cp!(path, dest)
{:ok, ~s(/uploads/#{Path.basename(dest)})}
end)
{:noreply, assign(socket, :uploaded_files, uploaded_files)}
end
defp safe_filename(original_name) do
ext = Path.extname(original_name)
"#{Ecto.UUID.generate()}#{ext}"
end<.form for={%{}} phx-submit="save" phx-change="validate" id="upload-form">
<div phx-drop-target={@uploads.photos.ref}>
<.live_file_input upload={@uploads.photos} />
</div>
<%= for entry <- @uploads.photos.entries do %>
<div>
<.live_img_preview entry={entry} />
<progress value={entry.progress} max="100"><%= entry.progress %>%</progress>
<%= for err <- upload_errors(@uploads.photos, entry) do %>
<p class="error"><%= error_to_string(err) %></p>
<% end %>
</div>
<% end %>
<button type="submit">Upload</button>
</.form>
def error_to_string(:too_large), do: "Too large"
def error_to_string(:too_many_files), do: "Too many files"
def error_to_string(:not_accepted), do: "Unacceptable file type"# lib/my_app_web endpoint.ex
def static_paths do
~w(assets fonts images favicon.ico robots.txt uploads)
end| ❌ Don't | ✅ Do |
|---|---|
Use auto_upload: true with form submission | Use manual uploads and consume on save |
Forget to add uploads to static_paths() | Add the dir to static_paths() (files 404 without it) |
Expect static_paths() edits to hot-reload | Restart the server after changing static_paths() |
| Trust the client-reported MIME type | Validate file types server-side |
| Keep the client's original filename | Generate a safe filename (e.g. Ecto.UUID.generate/0 + ext) |
| Swallow upload errors silently | Render error_to_string/1 per entry in the template |
Skip the "validate" handler | Implement handle_event("validate", ...) to track changes |
| Predecessor | This Skill | Successor |
|---|---|---|
| phoenix-liveview-essentials | phoenix-uploads | testing-essentials |
| apply-phoenix-liveview-conventions | phoenix-uploads | security-essentials |
Companion skills:
phoenix-liveview-essentials — LiveView lifecycle and form bindingsecurity-essentials — validating and safely serving user-supplied filestesting-essentials — exercising uploads with Phoenix.LiveViewTest.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators