CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/elixir-phoenix-skills

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).

73

Quality

91%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.mdskills/phoenix/phoenix-scopes/

name:
phoenix-scopes
type:
atomic
tags:
atomic
license:
MIT
description:
MANDATORY for Phoenix 1.8+ authentication and authorization. Covers Scope-based authentication replacing current_user, including Scope struct definition with roles and permissions, scope creation and usage in LiveViews and controllers, safe template access patterns, and step-by-step migration from current_user to scopes. Use when working with Phoenix 1.8+ authentication, authorization, Scope structs, current_scope, scope-based auth, roles, permissions, or migrating from current_user to the new scope-based model. Trigger words: Scope, current_scope, scopes, phoenix scopes, role, roles, permission, permissions, authorization, authorize, can?, authenticated?, anonymous, on_mount, require_scope.
metadata:
{"user-invocable":"true","version":"1.0.0"}

Phoenix Scopes

Phoenix 1.8 introduced Scope as the new authentication primitive, replacing direct current_user access.

RULES — Follow these with no exceptions

  1. Use bracket access in templatesassigns[:current_scope] prevents crashes when unauthenticated
  2. Test both authenticated and unauthenticated states — scope-based auth has two distinct code paths
  3. Define anonymous/0 for the unauthenticated case — return a Scope with user: nil

Scope Struct Definition

defmodule MyApp.Scope do
  defstruct [:user, :role, :permissions, :tenant]

  def for_user(%MyApp.Accounts.User{} = user) do
    %__MODULE__{
      user: user,
      role: user.role,
      permissions: permissions_for(user.role)
    }
  end

  def anonymous, do: %__MODULE__{user: nil}

  def authenticated?(%__MODULE__{user: nil}), do: false
  def authenticated?(%__MODULE__{}), do: true

  def can?(%__MODULE__{permissions: perms}, action) when is_list(perms), do: action in perms
  def can?(%__MODULE__{}, _action), do: false

  defp permissions_for(:admin), do: [:read, :write, :delete, :manage_users]
  defp permissions_for(:editor), do: [:read, :write]
  defp permissions_for(_), do: []
end

Using Scopes in LiveViews

defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    scope = socket.assigns.current_scope

    if Scope.authenticated?(scope) do
      {:ok, assign(socket, :posts, Blog.list_user_posts(scope))}
    else
      {:ok, push_navigate(socket, to: ~p"/login")}
    end
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    scope = socket.assigns.current_scope

    if Scope.can?(scope, :delete) do
      # perform delete
      {:noreply, socket}
    else
      {:noreply, put_flash(socket, :error, "Not authorized")}
    end
  end
end

Safe Template Access

<%= if assigns[:current_scope] && Scope.authenticated?(@current_scope) do %>
  <p>Welcome, <%= @current_scope.user.email %></p>
  <.link href={~p"/settings"}>Settings</.link>
<% else %>
  <.link href={~p"/login"}>Log in</.link>
<% end %>

Migration Workflow (current_user → Scopes)

  1. Define the Scope struct — create MyApp.Scope with for_user/1, anonymous/0, authenticated?/1, and can?/2 as shown above.
  2. Update on_mount hooks — replace user assignment with scope assignment (see before/after below). Run mix test test/my_app_web/live/ --trace and verify all on_mount tests pass before proceeding.
  3. Search and replace @current_user — update all template references to @current_scope.user; use assigns[:current_scope] for optional access. Run mix test and fix any KeyError or FunctionClauseError failures before continuing.
  4. Update context functions — pass scope instead of user to functions that need auth context. Re-run mix test to confirm no regressions.
  5. Run the full test suite — verify both authenticated and unauthenticated flows still work. If failures occur, revert the most recent step, fix the issue, and re-verify before moving forward.

Before (Phoenix 1.7)

def on_mount(:require_authenticated_user, _params, session, socket) do
  case get_user_from_session(session) do
    nil -> {:halt, redirect(socket, to: ~p"/login")}
    user -> {:cont, assign(socket, :current_user, user)}
  end
end

After (Phoenix 1.8)

def on_mount(:require_authenticated_user, _params, session, socket) do
  scope = get_scope_from_session(session)

  if Scope.authenticated?(scope) do
    {:cont, assign(socket, :current_scope, scope)}
  else
    {:halt, redirect(socket, to: ~p"/login")}
  end
end

Testing Scopes

Always test both authenticated and unauthenticated paths.

Unit Tests for Scope Predicates

defmodule MyApp.ScopeTest do
  use ExUnit.Case, async: true

  describe "authenticated?/1" do
    test "returns true for scope with user" do
      scope = %MyApp.Scope{user: build(:user)}
      assert Scope.authenticated?(scope) == true
    end

    test "returns false for anonymous scope" do
      assert Scope.authenticated?(Scope.anonymous()) == false
    end
  end

  describe "can?/2" do
    test "returns true when permission is present" do
      scope = %MyApp.Scope{permissions: [:read, :write]}
      assert Scope.can?(scope, :read) == true
    end

    test "returns false when permission is absent or scope is anonymous" do
      assert Scope.can?(%MyApp.Scope{permissions: [:read]}, :delete) == false
      assert Scope.can?(Scope.anonymous(), :read) == false
    end
  end
end

LiveView Tests

describe "DashboardLive" do
  test "shows dashboard content for authenticated user", %{conn: conn} do
    conn = log_in_user(conn, insert(:user))
    assert {:ok, _, html} = live(conn, "/dashboard")
    assert html =~ "Welcome"
  end

  test "redirects to login when unauthenticated", %{conn: conn} do
    assert {:error, {:redirect, %{to: "/login"}}} = live(conn, "/dashboard")
  end
end

skills

phoenix

phoenix-scopes

README.md

tile.json