Getting Started with AshBackpex

View Source

This guide walks you through setting up AshBackpex to create an admin interface for your Ash resources.

Prerequisites

  • An existing Phoenix application with Ash Framework configured
  • Backpex installed and configured (see Backpex documentation)
  • At least one Ash resource you want to administer

Installation

Add ash_backpex to your dependencies in mix.exs:

def deps do
  [
    {:ash_backpex, "~> 0.3.4"}
  ]
end

Run mix deps.get to install the dependency.

AshBackpex targets Backpex ~> 0.22.0 and declares that dependency itself. If your application pins Backpex directly, update its constraint to match.

Upgrading an existing admin?

AshBackpex 0.2 and 0.3 each follow a Backpex release with breaking changes. Read Upgrading to 0.3 before you bump the dependency.

Creating Your First Admin LiveResource

1. Ensure You Have an Admin Layout

If you followed the Backpex installation guide, you should already have an admin layout configured. AshBackpex uses the same layout system as Backpex.

If you need to create a new admin layout, Backpex provides the Backpex.HTML.Layout.app_shell/1 component as a foundation. Here's an example following Backpex conventions:

# lib/my_app_web/components/layouts.ex
defmodule MyAppWeb.Layouts do
  use MyAppWeb, :html

  import Backpex.HTML.Layout

  attr :flash, :map, required: true
  attr :fluid?, :boolean, default: false
  attr :current_url, :string, required: true
  attr :socket, :any, required: true
  attr :current_theme, :string, required: true
  attr :sidebar_open, :boolean, required: true
  attr :sidebar_section_states, :map, required: true
  attr :preferences_manifest, :map, required: true
  slot :inner_block, required: true

  def admin(assigns) do
    ~H"""
    <.app_shell
      socket={@socket}
      fluid={@fluid?}
      sidebar_open={@sidebar_open}
      preferences_manifest={@preferences_manifest}
    >
      <:sidebar_branding>
        <.sidebar_branding title="My App" />
      </:sidebar_branding>
      <:topbar>
        <.theme_selector
          current_theme={@current_theme}
          themes={[{"Light", "light"}, {"Dark", "dark"}]}
        />
        <.topbar_dropdown>
          <:label>
            <div class="btn btn-square btn-ghost">
              <Backpex.HTML.CoreComponents.icon name="hero-user" class="size-6" />
            </div>
          </:label>
          <li>
            <.link href={~p"/"} class="flex justify-between hover:bg-base-200">
              <p>Back to App</p>
            </.link>
          </li>
        </.topbar_dropdown>
      </:topbar>
      <:sidebar>
        <.sidebar_section id="blog" sidebar_section_states={@sidebar_section_states}>
          <:label>Blog</:label>
          <.sidebar_item current_url={@current_url} navigate={~p"/admin/posts"}>
            <Backpex.HTML.CoreComponents.icon name="hero-document-text" class="size-5" /> Posts
          </.sidebar_item>
          <%!-- Add more sidebar items for your resources --%>
        </.sidebar_section>
      </:sidebar>
      <.flash_messages flash={@flash} />
      {render_slot(@inner_block)}
    </.app_shell>
    """
  end
end

Backpex.InitAssigns assigns @current_theme, @sidebar_open, @sidebar_section_states, and @preferences_manifest; the layout passes them on. Two details of sidebar_section/1 are easy to miss:

  • id is required and must be unique among your sections. Backpex stores each section's open state under it. Use only letters, digits, underscores, and hyphens.
  • sidebar_section_states must be passed explicitly. A function component does not inherit the surrounding assign, so a section without it always renders open and a collapsed section does not stay collapsed.

Use the server-assigned theme in your root layout:

<html data-theme={assigns[:current_theme] || "light"}>

See the Backpex layout documentation for more details on available components and customization options.

2. Create a LiveResource

Create a LiveResource module that uses AshBackpex.LiveResource:

# lib/my_app_web/live/admin/post_live.ex
defmodule MyAppWeb.Admin.PostLive do
  use AshBackpex.LiveResource

  backpex do
    resource MyApp.Blog.Post
    layout {MyAppWeb.Layouts, :admin}

    fields do
      field :title
      field :content do
        module Backpex.Fields.Textarea
      end
      field :published
      field :inserted_at
    end
  end
end

3. Add Routes

Add routes for your admin LiveResource:

# lib/my_app_web/router.ex
import Backpex.Router

scope "/admin", MyAppWeb.Admin do
  pipe_through [:browser]

  backpex_routes()

  live_session :backpex_admin,
    on_mount: [{MyAppWeb.LiveUserAuth, :require_admin}, Backpex.InitAssigns] do
    live_resources "/posts", PostLive
  end
end

Put your authentication hook first. It must assign current_user, and halt the mount for anyone who may not use the admin:

# lib/my_app_web/live_user_auth.ex
defmodule MyAppWeb.LiveUserAuth do
  import Phoenix.Component
  import Phoenix.LiveView

  def on_mount(:require_admin, _params, session, socket) do
    socket = assign_new(socket, :current_user, fn -> load_user(session) end)

    if admin?(socket.assigns.current_user) do
      {:cont, socket}
    else
      {:halt, redirect(socket, to: "/sign-in")}
    end
  end

  # load_user/1 and admin?/1 depend on how your application authenticates.
end

The order matters for two reasons. AshBackpex uses assigns.current_user as the Ash actor, so it has to be there before the LiveResource mounts. And Backpex.InitAssigns reads the user's stored preferences using whatever the authentication hook assigned, so it has to run after it. If you use AshAuthentication or mix phx.gen.auth, use the on_mount hook they provide in place of MyAppWeb.LiveUserAuth.

4. Wire Up the Browser

Backpex sends the browser's stored preferences with the LiveView connection. Wrap your connect params with backpexParams in assets/js/app.js:

import { Hooks as BackpexHooks, backpexParams } from "backpex";

const liveSocket = new LiveSocket("/live", Socket, {
  params: backpexParams({ _csrf_token: csrfToken }),
  hooks: { ...BackpexHooks },
});

Without it, the theme, sidebar, and column choices revert when the user navigates to another resource, and Backpex logs a console warning.

5. Visit the Admin

Start your Phoenix server and visit http://localhost:4000/admin/posts to see your admin interface.

Admin Pages That Are Not Resources

A dashboard or any other LiveView can use the same admin layout. Route it inside the same live_session, so both hooks run for it:

live_session :backpex_admin,
  on_mount: [{MyAppWeb.LiveUserAuth, :require_admin}, Backpex.InitAssigns] do
  live "/", DashboardLive
  live_resources "/posts", PostLive
end

A LiveResource hands every assign to the layout for you. Your own LiveView has to pass them itself:

# lib/my_app_web/live/admin/dashboard_live.ex
defmodule MyAppWeb.Admin.DashboardLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <MyAppWeb.Layouts.admin
      socket={@socket}
      flash={@flash}
      current_url={@current_url}
      current_theme={@current_theme}
      sidebar_open={@sidebar_open}
      sidebar_section_states={@sidebar_section_states}
      preferences_manifest={@preferences_manifest}
    >
      <h1 class="text-2xl font-semibold">Dashboard</h1>
    </MyAppWeb.Layouts.admin>
    """
  end
end

socket, sidebar_open, sidebar_section_states, and preferences_manifest are the ones Backpex 0.20 added, and the ones an older dashboard will be missing. The layout above requires all of them, so the page raises a KeyError when one is left out.

Link to the page from the layout's <:sidebar> slot with another sidebar_item.

Adding More Features

Searchable Fields

Make fields searchable to enable the search box:

fields do
  field :title do
    searchable true
  end
  field :content do
    searchable true
    module Backpex.Fields.Textarea
  end
end

Relationships

Display relationships with navigation links:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}
  load [:author]  # Preload the relationship

  fields do
    field :title
    field :author do
      display_field :name
      live_resource MyAppWeb.Admin.UserLive
    end
  end
end

Filters

Add filters to the index view:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}

  fields do
    field :title
    field :published
    field :status
  end

  filters do
    filter :published do
      module Backpex.Filters.Boolean
    end
    filter :status do
      module Backpex.Filters.Select
    end
  end
end

Custom Sort Order

Set the default sort order:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}
  init_order %{by: :inserted_at, direction: :desc}

  fields do
    # ...
  end
end

Persisted Index State

Persistence is opt-in, and the default is to persist nothing. Choose which index settings should survive navigation and reloads:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}
  persist [:order, :filters, :columns, :metrics]

  fields do
    # ...
  end
end

Panels for Form Organization

Organize form fields into panels:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}

  panels [
    content: "Content",
    settings: "Settings"
  ]

  fields do
    field :title do
      panel :content
    end
    field :content do
      panel :content
      module Backpex.Fields.Textarea
    end
    field :published do
      panel :settings
    end
    field :status do
      panel :settings
    end
  end
end

Custom Display Names

Customize how resources are labeled:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}
  singular_name "Blog Post"
  plural_name "Blog Posts"

  fields do
    # ...
  end
end

Custom Item Actions

Add or remove item actions:

backpex do
  resource MyApp.Blog.Post
  layout {MyAppWeb.Layouts, :admin}

  item_actions do
    strip_default [:delete]  # Remove delete action
    action :archive, MyApp.ItemActions.Archive
  end

  fields do
    # ...
  end
end

Authorization

AshBackpex automatically integrates with Ash authorization policies. The admin will:

  • Check Ash.can?/2 before showing create/edit/delete buttons, and again before every create, update, delete, and item action
  • Use assigns.current_user as the actor for reads and relationship options
  • Persist create and update changesets with their configured actor

Make sure your Ash resources have policies defined and that you're setting current_user in your LiveView assigns. If you provide custom create or update changeset functions, set that actor on the returned Ash changeset.

Policies are for rules that depend on who is asking. When the admin should not offer an operation to anyone, turn it off in the LiveResource instead:

backpex do
  resource MyApp.Audit.Event
  layout {MyAppWeb.Layouts, :admin}

  create_action false   # no "New" button, and :new is denied
  update_action false   # no editing
  destroy_action false  # no deleting
end

Do this even if you also route the resource with except: [:new]. Backpex shows the "New" button on an empty index page whenever :new is allowed, without looking at the routes, and the page raises when the route is missing.

Do not define can?/3 in the LiveResource. AshBackpex generates it, and defining it is a compile error.

Deleting is authorized per record: Backpex checks the destroy action's policies for every selected record, as the current user, before anything is deleted. Backpex does not hand the actor to the delete callback itself, so the destroy action then runs without one. A change or notifier on that action that reads the actor (an audit trail, for example) will not see it.

Next Steps