The following guide will help you to install Backpex in your Phoenix application. We will guide you through the installation process and show you how to create a simple resource.

Prerequisites

Backpex integrates seamlessly with your existing Phoenix LiveView application, but there are a few prerequisites you need to meet before you can start using it.

Phoenix LiveView

Backpex is built on top of Phoenix LiveView, so you need to have Phoenix LiveView installed in your application. If you generate a new Phoenix application using the latest version of the mix phx.new generator, Phoenix LiveView is included by default.

Info

We have created an initial version of the Backpex installer task, which runs with the help of the Igniter framework. It automates many of the upcoming edits, including importing and registering the Backpex hooks. It does not rewrite your LiveSocket params; it prints a notice with the backpexParams edit you must apply manually. Review the generated diff and complete any remaining steps in this guide. Feedback is welcome!

Tailwind CSS

Backpex uses Tailwind CSS for styling. Make sure you have Tailwind CSS installed in your application. You can install Tailwind CSS by following the official installation guide. If you generate a new Phoenix application using the latest version of the mix phx.new generator, Tailwind CSS is included by default.

Note that the current version of Backpex requires Tailwind CSS version 4

Ecto

Backpex currently depends on Ecto as the database layer. Make sure you have a running Ecto repository in your application.

Warning

Backpex requires a single primary key field in your database schema. Compound keys are not supported. We tested Backpex with UUID (binary_id), integer (bigserial) and string primary keys. Note that the primary key is used in the URL for Show and Edit Views, so make sure it is always URL-encoded or safe to use in a URL.

If you meet all these prerequisites, you are ready to install and configure Backpex in your Phoenix application.

Add to list of dependencies

In your mix.exs:

defp deps do
  [
    ...
    {:backpex, "~> 0.19"}
  ]
end

Check hex.pm page for the latest version.

Global configuration

Set the PubSub server of your application in your config.exs:

config :backpex, :pubsub_server, MyApp.PubSub

See the Listen to PubSub Events guide for more info on how use and customize your PubSub configuration.

Backpex Hooks and connect params

Backpex comes with a few JS hooks which need to be included in your app.js, plus a params helper for your LiveSocket.

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

const Hooks = {} // your application hooks (optional)

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

backpexParams wraps your own connect params and adds the preferences the browser has stored for this tab. A LiveView reads the session snapshot taken when the websocket connected, so after a live_redirect it cannot see preferences written since — without the params, hidden columns and metrics reappear as soon as the user navigates. Passing a function (rather than a plain object) matters: LiveView re-evaluates it on every join, which is what keeps the values current. The connect params are one of two carriers for the browser's preference overlay — the other is the short-lived backpex_prefs cookie, which needs no wiring — and backpexParams remains required because it is the only carrier that exists on a live_redirect, which makes no HTTP request at all. See User Preferences for the full rationale.

The helper accepts a params object, not an existing params function. If your application already computes connect params dynamically, compose the preference params in your own function instead:

import { BackpexPreferences, Hooks as BackpexHooks } from 'backpex'

const appParams = () => ({
  _csrf_token: csrfToken,
  locale: document.documentElement.lang
})

const liveSocket = new LiveSocket('/live', Socket, {
  params: () => ({
    ...appParams(),
    ...BackpexPreferences.connectParams()
  }),
  hooks: { ...BackpexHooks }
})

daisyUI

Backpex is styled using daisyUI. Make sure you have daisyUI installed in your application. You can install daisyUI by following the official installation guide.

Note that the current version of Backpex requires daisyUI version 5.

Add files to Tailwind content

Backpex uses Tailwind CSS and daisyUI. Make sure to add Backpex files as a tailwind source in order to include the Backpex styles.

In your stylesheet:

@source "../../deps/backpex/**/*.*ex";
@source "../../deps/backpex/assets/js/**/*.*js";

Info

The path to the Backpex files may vary depending on your project setup.

Setup formatter

Backpex ships with a formatter configuration. To use it, add Backpex to the list of dependencies in your .formatter.exs.

# my_app/.formatter.exs
[
  import_deps: [:backpex]
]

Configure routing

To make LiveResources accessible in your application, you first need to configure your router (router.ex).

Backpex needs a backpex_preferences route in your router. This route is used by Backpex to persist user preferences such as the active theme and sidebar state.

Backpex provides a macro you can use to add the required routes to your router. Make sure to import Backpex.Router at the top of your router file or prefix the function calls.

Add this route in every independently scoped Backpex area that needs to persist preferences. A fixed /admin scope normally needs it once.

# router.ex

import Backpex.Router

scope "/admin", MyAppWeb do
  pipe_through :browser

  # add this line
  backpex_routes()
end

Place backpex_routes/0 behind the same authentication and application-scope plugs as the pages that use it. Always include the :browser pipeline.

For tenant-scoped preferences, put the endpoint below the tenant segment and run tenant authorization first:

scope "/tenants/:tenant", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user, :assign_tenant]

  backpex_routes()
end

Then pass the concrete endpoint path to app_shell because Backpex cannot fill dynamic route segments without application data. Also pass an explicit path when the router contains multiple preferences routes; Backpex deliberately fails fast instead of guessing which application's scope should receive the write.

<Backpex.HTML.Layout.app_shell
  socket={@socket}
  preferences_manifest={@preferences_manifest}
  preferences_path={~p"/tenants/#{@current_scope.tenant.id}/backpex_preferences"}
>
  ...
</Backpex.HTML.Layout.app_shell>

Create a default admin layout component

Although Backpex does not ship with a predefined layout component, it does provide components that you can use to build your own layout component. You can find all Backpex components in the lib/backpex/html directory of our GitHub repository (see the snippet below for a pre-built layout that you can copy & paste into your application).

Warning

Note that some components are tied to Backpex and therefore might not be used outside of it. Our goal is to make them more generic in the future so all Backpex components can easily be used in custom views, too.

To get you started quickly, we provide a layout component you can copy & paste into your application. Place it as a file in your lib/myapp_web/templates/layout directory. You can name it whatever you like, but we recommend using admin.html.heex. You can also use this component as the only layout in your application if your application consists of only an admin interface. This layout component uses the Backpex.HTML.Layout.app_shell/1 component, which can be used to easily add an app shell layout to your application.

<Backpex.HTML.Layout.app_shell
  socket={@socket}
  fluid={@fluid?}
  live_resource={@live_resource}
  sidebar_open={@sidebar_open}
  preferences_manifest={@preferences_manifest}
>
  <:topbar>
    <div class="flex-1"></div>

    <Backpex.HTML.Layout.topbar_dropdown class="mr-2 md:mr-0">
      <:label>
        <div class="btn btn-square btn-ghost">
          <Backpex.HTML.CoreComponents.icon name="hero-user" class="size-6" />
        </div>
      </:label>
      <li>
        <.link href="/" class="text-error flex justify-between hover:bg-base-200">
          <p>Logout</p>
          <Backpex.HTML.CoreComponents.icon name="hero-arrow-right-on-rectangle" class="size-5" />
        </.link>
      </li>
    </Backpex.HTML.Layout.topbar_dropdown>
  </:topbar>
  <:sidebar_branding>
    <Backpex.HTML.Layout.sidebar_branding />
  </:sidebar_branding>
  <:sidebar>
    <!-- Sidebar Content -->
  </:sidebar>
  <Backpex.HTML.Layout.flash_messages flash={@flash} />
  {render_slot(@inner_block)}
</Backpex.HTML.Layout.app_shell>

These assigns (@current_theme, @sidebar_open, @sidebar_section_states, @preferences_manifest) are populated by Backpex.InitAssigns — see Add resource routes below for setup.

@preferences_manifest contains one opaque, server-signed namespace token per configured adapter route. Backpex uses it to keep session-backed values across tenant navigation while discarding values whose adapter namespace changed. Pass it through; without it Backpex disables browser overlays, so live navigation or a toggle-and-reload inside the write's round trip may initially render the previous stored value (see Why the client sometimes overrides the server).

In addition we recommend to add a bodyless function definition and to configure declarative assigns for your layout component.

defmodule MyAppWeb.Layouts do
  use MyAppWeb, :html

  embed_templates "layouts/*"

  attr :flash, :map, required: true, doc: "the map of flash messages"
  attr :fluid?, :boolean, default: true, doc: "if the content uses full width"
  attr :current_url, :string, required: true, doc: "the current url"
  attr :current_theme, :string, default: nil, doc: "the currently selected theme"
  attr :sidebar_open, :boolean, default: true, doc: "initial sidebar open state"
  attr :sidebar_section_states, :map, default: %{}, doc: "map of sidebar section open states"
  attr :preferences_manifest, :map, default: nil, doc: "signed client namespace manifest"

  slot :inner_block, required: true

  def admin(assigns)
end

Make sure to always add the Backpex.HTML.Layout.flash_messages component to display flash messages in your layout component and do not forget to render the :inner_content slot to insert the content of the LiveView.

We use the icon/1 component to render icons in the layout component. The icon/1 component is part of the core_components module that ships with new Phoenix projects. See (core_components.ex)(https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex). Feel free to use your own icon component or library.

Information

The Backpex.HTML.Layout.app_shell/1 component accepts a boolean fluid to determine if a LiveResource should be rendered full width. There is a fluid? option you can configure in a LiveResource. See the Fluid Layout documentation for more information.

Remove default background color

If you start with a new Phoenix project, you may have a default background color set for your body tag. This conflicts with the background color of the Backpex app_shell.

So if you have this in your root.html.heex.

<body class="bg-white">
</body>

You should remove the bg-white class.

If you need this color on your body tag to style your application, consider using another root layout for Backpex (see put_root_layout/2).

Remove @tailwindcss/forms plugin

There is a conflict between the @tailwindcss/forms plugin and daisyUI. You should remove the @tailwindcss/forms plugin to prevent styling issues.

// remove this line
@plugin "tailwindcss/forms;

If your application depends on the @tailwindcss/forms plugin, you can keep the plugin and change the strategy to 'class'. This will prevent the plugin from conflicting with daisyUI. Note that you then have to add the form classes provided by the @tailwindcss/forms plugin to your inputs manually.

Create an example resource

To make it more practical, we are going to create a simple resource that we will use in all our examples later in the installation guide. You can skip this step if you want to use your own resource or just follow the guide.

The example resource will be a Post resource with the following fields:

  • title (string)
  • views (integer)

Run the following commands:

$ mix phx.gen.schema Blog.Post blog_posts title:string views:integer
$ mix ecto.migrate

These commands will generate a Post schema and a migration file. The migration file will create a blog_posts table in your database.

You are now prepared to set up the Backpex layout component and a LiveResource for the Post resource.

Configure LiveResource

To create a LiveResource for the Post resource, you need to create LiveResource module.

defmodule MyAppWeb.Live.PostLive do
  use Backpex.LiveResource,
    adapter_config: [
      schema: MyApp.Blog.Post,
      repo: MyApp.Repo,
      update_changeset: &MyApp.Blog.Post.update_changeset/3,
      create_changeset: &MyApp.Blog.Post.create_changeset/3
    ],
    layout: {MyAppWeb.Layouts, :admin}
end

Backpex.LiveResource is the module that will generate the corresponding LiveViews for the resource you configured. We provide a macro you have to use to configure the LiveResource. You are required to set some general options to tell Backpex where to find the resource and what changesets should be used. The above example shows the configuration for a Post resource.

All options you can see in the above example are required:

  • The layout option tells Backpex which layout component to use for the LiveResource. In this case, we use the :admin(admin.html.heex) component created in the previous step.
  • The schema option tells Backpex which schema to use for the resource.
  • The repo option tells Backpex which repo to use for the resource.
  • The update_changeset and create_changeset options tell Backpex which changesets to use for updating and creating the resource.
  • The pubsub option tells Backpex which pubsub options to use for the resource (see the Listen to PubSub Events guide for more information).

If your primary key is not named "id", you are also required to set the primary_key option:

use Backpex.LiveResource,
  adapter_config: [
    ...
  ],
  primary_key: :code

In addition to the required options, you pass to the Backpex.LiveResource macro, you are required to implement the following callback functions in the module:

  • singular_name/0 - This function should return the singular name of the resource.
  • plural_name/0 - This function should return the plural name of the resource.
  • fields/0 - This function should return a list of fields to display in the LiveResource.

After implementing the required callback functions, our PostLive module looks like this:

defmodule MyAppWeb.Live.PostLive do
  use Backpex.LiveResource,
    adapter_config: [
      schema: MyApp.Blog.Post,
      repo: MyApp.Repo,
      update_changeset: &MyApp.Blog.Post.update_changeset/3,
      create_changeset: &MyApp.Blog.Post.create_changeset/3
    ],
    layout: {MyAppWeb.Layouts, :admin}

  @impl Backpex.LiveResource
  def singular_name, do: "Post"

  @impl Backpex.LiveResource
  def plural_name, do: "Posts"

  @impl Backpex.LiveResource
  def fields do
    [
      title: %{
        module: Backpex.Fields.Text,
        label: "Title"
      },
      views: %{
        module: Backpex.Fields.Number,
        label: "Views"
      }
    ]
  end
end

The fields/0 function returns a list of fields to display in the LiveResource. See What is a Field? for more information.

Information

We recommend placing the LiveResource in the lib/myapp_web/live directory. You can name the module like you want, but in this case, we recommend using post_live.ex.

Configure resource routing

Add resource routes

To make LiveResources accessible in your application, you need to add routes for them. Backpex makes it easy to add the required routes to your router by providing the live_resources/3 macro.

Furthermore, Backpex provides a Backpex.InitAssigns module / hook. This will attach the current_url to the LiveView. Backpex needs it to highlight the current sidebar item in the layout component. You can also use your own init assigns module if you want to attach more assigns to the LiveView, but make sure to add the current_url to the assigns.

In the following example, we use the Phoenix.LiveView.Router.live_session/3 function to add the Backpex.InitAssigns Hook to all LiveViews in the /admin scope. This is our recommended way, but you could also add the on_mount Hook to manually to your LiveResources.

# router.ex

import Backpex.Router

scope "/admin", MyAppWeb do
  pipe_through :browser

  backpex_routes()

  # add these lines
  live_session :default, on_mount: Backpex.InitAssigns do
    live_resources "/posts", PostLive
  end
end

The live_resources/3 macro will add the required routes for the PostLive module. You can now access the PostLive LiveResource at /admin/posts.

You probably also want to add link to your created LiveResource in the sidebar. For this, Backpex provides the Backpex.HTML.Layout.sidebar_item/1 component.

If you copied the provided layout component from the section above, you can just use the sidebar_item/1 component inside the sidebar slot like this:

<Backpex.HTML.Layout.app_shell
  socket={@socket}
  fluid={@fluid?}
  live_resource={@live_resource}
  sidebar_open={@sidebar_open}
  preferences_manifest={@preferences_manifest}
>
  <:topbar>
    <!-- Topbar Content -->
  </:topbar>
  <:sidebar>
    <!-- Add sidebar items -->
    <Backpex.HTML.Layout.sidebar_item current_url={@current_url} navigate={~p"/admin/posts"}>
      <.icon name="hero-book-open" class="size-5" /> Posts
    </Backpex.HTML.Layout.sidebar_item>
  </:sidebar>
  <Backpex.HTML.Layout.flash_messages flash={@flash} />
  {render_slot(@inner_block)}
</Backpex.HTML.Layout.app_shell>

You can also group sidebar items into collapsible sections using the Backpex.HTML.Layout.sidebar_section/1 component:

<:sidebar>
  <Backpex.HTML.Layout.sidebar_section id="blog" sidebar_section_states={@sidebar_section_states}>
    <:label>Blog</:label>
    <Backpex.HTML.Layout.sidebar_item current_url={@current_url} navigate={~p"/admin/posts"}>
      <.icon name="hero-book-open" class="size-5" /> Posts
    </Backpex.HTML.Layout.sidebar_item>
    <Backpex.HTML.Layout.sidebar_item current_url={@current_url} navigate={~p"/admin/categories"}>
      <.icon name="hero-tag" class="size-5" /> Categories
    </Backpex.HTML.Layout.sidebar_item>
  </Backpex.HTML.Layout.sidebar_section>
</:sidebar>

The sidebar_section_states assign (provided by Backpex.InitAssigns) tracks which sections are expanded or collapsed, and persists user preferences across page loads.

Empty sections are hidden by CSS before the first paint. Use sidebar_item/1 for their leaf entries (including entries inside nested sections); it adds the data-sidebar-item marker that makes every non-empty ancestor visible. If you render a custom leaf instead, add data-sidebar-item to its root element.

Configure a default route

In case you want a default route for /admin we recommend creating a redirect controller such as the following:

In my_app_web/controller create a file named redirect_controller.ex:

# redirect_controller.ex

defmodule MyAppWeb.RedirectController do
  use MyAppWeb, :controller

  def redirect_to_posts(conn, _params) do
    conn
    |> Phoenix.Controller.redirect(to: ~p"/admin/posts")
    |> Plug.Conn.halt()
  end
end

And configure in your router.ex file:

#router.ex

scope "/admin", MyAppWeb do
  pipe_through :browser

  backpex_routes()

  # add this line
  get "/", RedirectController, :redirect_to_posts

  live_session :default, on_mount: Backpex.InitAssigns do
    live_resources "/posts", PostLive
  end
end

Provide a Tailwind CSS plugin for Heroicons

Backpex uses the heroicons icon set. Backpex provides a Backpex.HTML.CoreComponents.icon/1 component, but you need to provide the icons and a Tailwind CSS plugin to generate the necessary styles to display them. If you generated your Phoenix project with the latest version of the mix phx.new generator, you already have the dependency and plugin installed. If not, follow the steps below.

Track the heroicons GitHub repository

Track the heroicons GitHub repository with Mix:

def deps do
  [
    ...
    {:heroicons,
      github: "tailwindlabs/heroicons",
      tag: "v2.1.1",
      sparse: "optimized",
      app: false,
      compile: false,
      depth: 1}
  ]
end

This will add the heroicons repository as a dependency to your project. You can find the optimized SVG icons in the deps/heroicons directory.

Add the Tailwind CSS plugin

Define the following plugin and import it into your stylesheet to generate the necessary styles to display the icons.

// tailwind.heroicons.js

// add fs, plugin and path to the top of the file
const plugin = require('tailwindcss/plugin')
const fs = require('fs')
const path = require('path')

module.exports = plugin(function ({ matchComponents, theme }) {
  const iconsDir = path.join(__dirname, '../../deps/heroicons/optimized')
  const values = {}
  const icons = [
    ['', '/24/outline'],
    ['-solid', '/24/solid'],
    ['-mini', '/20/solid'],
    ['-micro', '/16/solid']
  ]
  icons.forEach(([suffix, dir]) => {
    fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
      const name = path.basename(file, '.svg') + suffix
      values[name] = { name, fullPath: path.join(iconsDir, dir, file) }
    })
  })
  matchComponents(
    {
      hero: ({ name, fullPath }) => {
        let content = fs
          .readFileSync(fullPath)
          .toString()
          .replace(/\r?\n|\r/g, '')
        content = encodeURIComponent(content)
        let size = theme('spacing.6')
        if (name.endsWith('-mini')) {
          size = theme('spacing.5')
        } else if (name.endsWith('-micro')) {
          size = theme('spacing.4')
        }
        return {
          [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
          '-webkit-mask': `var(--hero-${name})`,
          mask: `var(--hero-${name})`,
          'mask-repeat': 'no-repeat',
          'background-color': 'currentColor',
          'vertical-align': 'middle',
          display: 'inline-block',
          width: size,
          height: size
        }
      }
    },
    { values }
  )
})
@plugin "./tailwind_heroicons.js";

This plugin will generate the necessary styles to display the heroicons in your application. You can now use the Backpex.HTML.CoreComponents.icon/1 component to render the icons in your application.

For example, to render the user icon, you can use the following code:

<Backpex.HTML.CoreComponents.icon name="hero-user" class="size-5" />

Set daisyUI theme

Backpex supports daisyUI themes. The following steps will guide you through setting up daisyUI themes in your application and optionally adding a theme selector to your layout component.

1. Add the themes to your application.

First, you need to add the themes to your stylesheet. You can add the themes to the daisyui plugin options. The following example shows how to add the light, dark, and cyberpunk themes to your application.

@plugin "daisyui" {
  themes: dark, cyberpunk;
}

@plugin "daisyui/theme" {
  name: "light";

  --color-primary: #1d4ed8;
  --color-primary-content: white;
  --color-secondary: #f39325;
  --color-secondary-content: white;
}

The full list of themes can be found at the daisyUI website.

2. Set the default daisyUI theme in your root layout.

Set the data-theme attribute on the html tag using the @current_theme assign. The Backpex.InitAssigns hook (which you should already have configured in your router) will automatically populate this assign from the user's saved preference.

# root.html.heex
<html data-theme={assigns[:current_theme] || "light"}>
  ...
</html>

If you just want to use a single theme, you can set the data-theme attribute to the theme name. You can skip the next steps and are done with the theme setup.

# root.html.heex
<html data-theme="light">
  ...
</html>

3. Add the theme selector component to the app shell

You can add a theme selector to your layout component to allow users to change the theme. The following example shows how to add a theme selector to the admin component. The list of themes should match the themes you added to your stylesheet.

<Backpex.HTML.Layout.app_shell
  socket={@socket}
  fluid={@fluid?}
  live_resource={@live_resource}
  sidebar_open={@sidebar_open}
  preferences_manifest={@preferences_manifest}
>
  <:topbar>
    <div class="flex-1"></div>

    <!-- Add this -->
    <Backpex.HTML.Layout.theme_selector
      current_theme={@current_theme}
      themes={[
        {"Light", "light"},
        {"Dark", "dark"},
        {"Cyberpunk", "cyberpunk"}
      ]}
    />

    <Backpex.HTML.Layout.topbar_dropdown>
      <!-- Topbar Dropdown -->
    </Backpex.HTML.Layout.topbar_dropdown>
  </:topbar>
  <:sidebar_branding>
    <Backpex.HTML.Layout.sidebar_branding />
  </:sidebar_branding>
  <:sidebar>
    <!-- Sidebar Content -->
  </:sidebar>
  <Backpex.HTML.Layout.flash_messages flash={@flash} />
  {render_slot(@inner_block)}
</Backpex.HTML.Layout.app_shell>

Theme changes are automatically persisted through the adapter configured for global.theme (the Phoenix session in a zero-config installation). Subsequent page loads are server-rendered from that stored value. A bounded pending cookie covers most reloads that race the write; without a signed namespace manifest or when the pending entry exceeds its cookie budget, the initial paint may briefly show the previous stored theme.