# Create Live Resource

> Use when scaffolding a new Backpex LiveResource, setting up admin CRUD views, or configuring adapter_config, fields, filters, and routing for a resource.

- Skill: `naymspace/create-live-resource` (Agent Skill)
- Install (CLI): `npx skillmds@latest add naymspace/create-live-resource`
- Raw SKILL.md: https://api.skillmd.com/api/skills/naymspace/create-live-resource/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: naymspace (https://skillmd.com/u/naymspace)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/naymspace/create-live-resource

---


# Creating a Backpex LiveResource

You are an expert at creating LiveResources for Backpex. When the user wants to create a new admin resource, follow this process:

1. **Identify the Ecto schema** the resource will manage
2. **Generate the LiveResource module** with adapter_config, fields, and callbacks
3. **Add the route** to the router

## LiveResource Module Structure

```elixir
defmodule MyAppWeb.PostLive do
  use Backpex.LiveResource,
    adapter_config: [
      schema: MyApp.Post,
      repo: MyApp.Repo,
      update_changeset: &MyApp.Post.changeset/3,
      create_changeset: &MyApp.Post.changeset/3
    ]

  @impl Backpex.LiveResource
  def layout(_assigns), do: {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",
        searchable: true
      }
    ]
  end
end
```

## `use Backpex.LiveResource` Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `adapter_config` | keyword | **required** | Ecto adapter configuration (see below) |
| `adapter` | atom | `Backpex.Adapters.Ecto` | Data layer adapter |
| `primary_key` | atom | `:id` | Primary key field |
| `per_page_options` | list | `[15, 50, 100]` | Selectable page sizes |
| `per_page_default` | integer | `15` | Default page size |
| `init_order` | map or fn | `%{by: :id, direction: :asc}` | Initial sort order |
| `fluid?` | boolean | `false` | Full-width layout |
| `full_text_search` | atom | `nil` | PostgreSQL tsvector column name |
| `save_and_continue_button?` | boolean | `false` | Show "Save & Continue" button |
| `pubsub` | keyword | nil (falls back to `:backpex, :pubsub_server` app config, topic defaults to module name) | `[server: MyApp.PubSub]` |
| `on_mount` | atom/list | nil | LiveView on_mount hooks |

## adapter_config (Ecto)

| Key | Required | Description |
|-----|----------|-------------|
| `schema` | yes | Ecto schema module |
| `repo` | yes | Ecto repo module |
| `update_changeset` | no | `fn item, attrs, metadata -> changeset` |
| `create_changeset` | no | `fn item, attrs, metadata -> changeset` |
| `item_query` | no | `fn query, live_action, assigns -> query` |

The `metadata` keyword list contains `:assigns` and `:target` (the form field that triggered the change).

The `item_query` function must build on the incoming `query` argument (use `from p in query, ...`), not the schema directly.

## Required Callbacks

| Callback | Returns | Description |
|----------|---------|-------------|
| `singular_name/0` | string | e.g. `"Post"` |
| `plural_name/0` | string | e.g. `"Posts"` |
| `fields/0` | keyword list | Field definitions |
| `layout/1` | `{module, :function}` or `fn assigns -> ...` | Layout to use |

## Optional Callbacks

| Callback | Default | Description |
|----------|---------|-------------|
| `can?/3` | always true | `fn assigns, action, item -> bool` |
| `filters/0` | `[]` | Filter definitions |
| `filters/1` | delegates to `filters/0` | Assigns-aware variant for dynamic filters |
| `panels/0` | `[]` | Field grouping: `[key: "Label"]` |
| `metrics/0` | `[]` | Index page metrics |
| `resource_actions/0` | `[]` | Resource-level actions |
| `item_actions/1` | returns default_actions unchanged | Modify default item actions |
| `on_item_created/2` | noop | `fn socket, item -> socket` |
| `on_item_updated/2` | noop | `fn socket, item -> socket` |
| `on_item_deleted/2` | noop | `fn socket, item -> socket` |
| `return_to/5` | index page | Custom redirect after save |
| `index_row_class/4` | nil | Custom CSS for table rows |
| `render_resource_slot/3` | default HTML | Override UI slots |
| `translate/1` | delegates to `Backpex.translate/1` | Override UI strings |

## Router Setup

```elixir
import Backpex.Router

scope "/admin", MyAppWeb do
  pipe_through :browser

  backpex_routes()

  live_session :admin, on_mount: Backpex.InitAssigns do
    live_resources "/posts", PostLive
    live_resources "/users", UserLive
    live_resources "/categories", CategoryLive, only: [:index, :show]
  end
end
```

`backpex_routes()` must appear once per scope. `live_resources/3` generates routes for Index, Form (new/edit), and Show views.

Options for `live_resources/3`:
- `only: [:index, :show, :new, :edit]` to restrict routes
- `except: [:new]` to exclude specific routes

## Complete Example

```elixir
defmodule MyAppWeb.ProductLive do
  use Backpex.LiveResource,
    adapter_config: [
      schema: MyApp.Product,
      repo: MyApp.Repo,
      update_changeset: &MyApp.Product.changeset/3,
      create_changeset: &MyApp.Product.changeset/3,
      item_query: &__MODULE__.item_query/3
    ],
    per_page_default: 25,
    init_order: %{by: :inserted_at, direction: :desc}

  import Ecto.Query

  @impl Backpex.LiveResource
  def layout(_assigns), do: {MyAppWeb.Layouts, :admin}

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

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

  @impl Backpex.LiveResource
  def panels do
    [details: "Details", metadata: "Metadata"]
  end

  @impl Backpex.LiveResource
  def fields do
    [
      name: %{
        module: Backpex.Fields.Text,
        label: "Name",
        searchable: true,
        panel: :details
      },
      price: %{
        module: Backpex.Fields.Currency,
        label: "Price",
        panel: :details
      },
      category: %{
        module: Backpex.Fields.BelongsTo,
        label: "Category",
        display_field: :name,
        live_resource: MyAppWeb.CategoryLive,
        panel: :details
      },
      published: %{
        module: Backpex.Fields.Boolean,
        label: "Published",
        index_editable: true
      },
      inserted_at: %{
        module: Backpex.Fields.DateTime,
        label: "Created At",
        only: [:index, :show],
        panel: :metadata
      }
    ]
  end

  @impl Backpex.LiveResource
  def can?(_assigns, :delete, item), do: not item.published
  def can?(_assigns, _action, _item), do: true

  def item_query(query, _live_action, _assigns) do
    from p in query, where: is_nil(p.archived_at)
  end
end
```

## Conventions

- **File location**: `lib/my_app_web/live/<resource>_live.ex`
- **Module naming**: `MyAppWeb.<Resource>Live` (e.g. `MyAppWeb.ProductLive`)
- **Changeset functions** should accept 3 arguments: `item`, `attrs`, `metadata`
- **Use `layout/1` callback** instead of the `layout:` option (avoids compile-time dependencies)
- **Always build on the incoming query** in `item_query/3`

