guides/getting-started.md

# Getting Started with PhoenixFilament

PhoenixFilament lets you go from an Ecto schema to a fully-functional admin interface in minutes.
This guide walks you through installation, your first resource, and common customizations.

## Prerequisites

Before starting, you need:

- A Phoenix 1.7+ application with at least one Ecto schema
- Phoenix LiveView installed and configured
- Tailwind CSS configured (Phoenix 1.7+ includes it by default)
- daisyUI 5 (optional but recommended — Phoenix 1.8 includes it by default)

If you are on Phoenix 1.8, all of the above are included out of the box.

## Installation

### 1. Add dependencies

Add PhoenixFilament and Igniter to your `mix.exs`:

```elixir
def deps do
  [
    {:phoenix_filament, "~> 0.1"},
    {:igniter, "~> 0.7"}  # required for the installer
  ]
end
```

Fetch your dependencies:

```bash
mix deps.get
```

### 2. Run the installer

```bash
mix phx_filament.install
```

The installer is idempotent — safe to run multiple times.

## What the Installer Creates

Running `mix phx_filament.install` creates three files:

**`lib/my_app_web/admin.ex`** — your Panel module:

```elixir
defmodule MyAppWeb.Admin do
  use PhoenixFilament.Panel,
    path: "/admin",
    brand_name: "MyApp Admin"

  # Add resources with: mix phx_filament.gen.resource MyApp.Schema
end
```

**`assets/vendor/chart.min.js`** — the Chart.js library for chart widgets.

**`assets/js/phx_filament_hooks.js`** — the LiveView hook that powers Chart widgets.

## Router Setup

Add two lines to your `router.ex`:

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  # 1. Import the panel router macro
  import PhoenixFilament.Panel.Router

  pipeline :browser do
    # ... your existing pipeline
  end

  scope "/" do
    pipe_through :browser

    # 2. Mount the panel
    phoenix_filament_panel "/admin", MyAppWeb.Admin
  end
end
```

The `phoenix_filament_panel/2` macro registers all CRUD routes, the dashboard, and any
plugin routes automatically.

### Icons

PhoenixFilament uses [Heroicons](https://heroicons.com) for sidebar and widget icons.
Icons are referenced by their CSS class name (e.g., `hero-document-text`).

Phoenix 1.8 apps include Heroicons by default through the `assets/vendor/heroicons/`
directory. If your app was generated with Phoenix 1.8, icons work out of the box.

For older Phoenix apps or custom setups, ensure Heroicons are available:

1. The `heroicons` package must be in your assets
2. CSS classes like `hero-document-text` must resolve to SVG icons
3. See the [Phoenix Components documentation](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) for setup details

When registering resources, specify icons with the `hero-` prefix:

```elixir
resources do
  resource MyAppWeb.Admin.PostResource,
    icon: "hero-document-text"

  resource MyAppWeb.Admin.UserResource,
    icon: "hero-users"
end
```

Resources without an `icon:` option display the first letter of their label as a fallback.

## Hook Setup

Open `assets/js/app.js` and import the PhoenixFilament hooks:

```javascript
import PhxFilamentHooks from "./phx_filament_hooks"

// Find your LiveSocket initialization and merge the hooks:
let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  // Merge PhoenixFilament hooks with any existing hooks:
  hooks: {...Hooks, ...PhxFilamentHooks}
})
```

If you don't have any custom hooks yet, the hooks object will simply be `PhxFilamentHooks`.

## Your First Resource

Generate a resource for an existing Ecto schema:

```bash
mix phx_filament.gen.resource MyApp.Blog.Post
```

This creates `lib/my_app_web/admin/post_resource.ex`:

```elixir
defmodule MyAppWeb.Admin.PostResource do
  use PhoenixFilament.Resource,
    schema: MyApp.Blog.Post,
    repo: MyApp.Repo
end
```

### Resource registration in your Panel

`mix phx_filament.gen.resource` automatically registers the resource in your Panel module. You should see a `resources do...end` block added to `lib/my_app_web/admin.ex`:

```elixir
defmodule MyAppWeb.Admin do
  use PhoenixFilament.Panel,
    path: "/admin",
    brand_name: "MyApp Admin"

  resources do
    resource MyAppWeb.Admin.PostResource,
      icon: "hero-document-text",
      nav_group: "Blog"
  end
end
```

If auto-registration fails (e.g. the Panel module cannot be found), the task prints instructions for manual registration — simply add the `resources do...end` block above yourself.

## Visit /admin

Start your Phoenix server:

```bash
mix phx.server
```

Navigate to `http://localhost:4000/admin`. You will see:

- A sidebar with "Posts" listed under "Blog"
- An index page with a table of all posts, with search, sort, and pagination
- Create / Edit / View / Delete actions out of the box

PhoenixFilament auto-derives columns and form fields from your Ecto schema — no additional
configuration required to get a working interface.

## Customizing Forms

By default, PhoenixFilament infers form fields from your schema's Ecto field types. To
customize, add a `form do...end` block to your resource:

```elixir
defmodule MyAppWeb.Admin.PostResource do
  use PhoenixFilament.Resource,
    schema: MyApp.Blog.Post,
    repo: MyApp.Repo,
    label: "Post",
    plural_label: "Posts"

  form do
    text_input :title, label: "Title", placeholder: "Enter post title"
    textarea :body, label: "Body"
    toggle :published, label: "Published"
    date :published_at, label: "Publish Date"
  end
end
```

### Available field types

| Macro | Description |
|-------|-------------|
| `text_input :name` | Single-line text input |
| `textarea :name` | Multi-line text area |
| `number_input :name` | Numeric input |
| `select :name, options: [...]` | Drop-down select |
| `checkbox :name` | Checkbox (boolean) |
| `toggle :name` | Toggle switch (boolean) |
| `date :name` | Date picker |
| `datetime :name` | Date + time picker |
| `hidden :name` | Hidden field |

All field macros accept these common options:

- `label:` — override the auto-derived label
- `placeholder:` — input placeholder text

### Form sections

Group related fields with `section`:

```elixir
form do
  section "Basic Info" do
    text_input :title
    textarea :body
  end

  section "Publishing" do
    toggle :published
    date :published_at
  end
end
```

### Multi-column layout

Use `columns` to render fields side by side:

```elixir
form do
  columns 2 do
    text_input :first_name
    text_input :last_name
  end

  textarea :bio
end
```

### Conditional field visibility

Show or hide a field based on another field's value using `visible_when`:

```elixir
form do
  toggle :published
  date :published_at, visible_when: [field: :published, eq: true]
end
```

`visible_when` accepts `field:` (the field to watch) and `eq:` (the value that makes this
field visible). The check happens in real-time as the user fills in the form.

You can also apply `visible_when` to an entire section:

```elixir
form do
  toggle :published

  section "Schedule", visible_when: [field: :published, eq: true] do
    date :published_at
    select :timezone, options: ["UTC", "America/New_York", "Europe/Berlin"]
  end
end
```

## Customizing Tables

Add a `table do...end` block to your resource to customize the index listing:

```elixir
defmodule MyAppWeb.Admin.PostResource do
  use PhoenixFilament.Resource,
    schema: MyApp.Blog.Post,
    repo: MyApp.Repo

  table do
    column :title, label: "Title"
    column :published, label: "Published"
    column :inserted_at, label: "Created"

    actions do
      action :view,   label: "View",   icon: "hero-eye"
      action :edit,   label: "Edit",   icon: "hero-pencil"
      action :delete, label: "Delete", icon: "hero-trash", confirm: "Are you sure?"
    end

    filters do
      boolean_filter :published, label: "Published"
      select_filter :author_id, label: "Author", options: [{"Alice", 1}, {"Bob", 2}]
    end
  end
end
```

### Columns

Each `column` declaration renders a column in the index table.

```elixir
column :field_name                          # auto-derives label
column :field_name, label: "Custom Label"  # explicit label
```

### Actions

The `actions do...end` block defines the per-row action buttons.

```elixir
actions do
  action :view                                           # View action (shows the record)
  action :edit                                           # Edit action (opens edit form)
  action :delete, confirm: "Delete this post?"          # Delete with confirmation dialog
  action :archive, label: "Archive", icon: "hero-archive-box"  # Custom action
end
```

Built-in action types (`:view`, `:edit`, `:delete`) are handled automatically.
Custom action types dispatch `{:table_action, action, id}` to your resource's `handle_info/2`,
which you can override:

```elixir
defmodule MyAppWeb.Admin.PostResource do
  use PhoenixFilament.Resource,
    schema: MyApp.Blog.Post,
    repo: MyApp.Repo

  @impl true
  def handle_info({:table_action, :archive, id}, socket) do
    MyApp.Blog.archive_post(id)
    {:noreply, Phoenix.LiveView.put_flash(socket, :info, "Post archived")}
  end

  def handle_info(msg, socket), do: super(msg, socket)
end
```

### Filters

The `filters do...end` block renders a filter toolbar above the table.

```elixir
filters do
  boolean_filter :published                              # true / false toggle
  select_filter  :status, options: [{"Draft", "draft"}, {"Published", "published"}]
  date_filter    :inserted_at, label: "Created After"   # date range picker
end
```

### Search

Full-text search across string columns is enabled by default. The search box appears in the
table header and searches across all `:string` fields in your schema.

### Pagination and Sorting

Pagination and column sorting are enabled automatically. Clicking any column header toggles
ascending/descending sort. Pagination controls appear below the table.

## Dashboard Widgets

The dashboard (the page you land on at `/admin`) supports four widget types:
`StatsOverview`, `Chart`, `Table`, and `Custom`.

### StatsOverview widget

Shows stat cards with optional icons, colors, and sparklines.

```elixir
defmodule MyAppWeb.Admin.OverviewStats do
  use PhoenixFilament.Widget.StatsOverview

  @impl true
  def stats(_assigns) do
    [
      stat("Total Posts", MyApp.Repo.aggregate(MyApp.Blog.Post, :count),
        icon: "hero-document-text",
        color: :success,
        description: "#{new_today()} new today"),

      stat("Published", published_count(),
        icon: "hero-check-circle",
        color: :info),

      stat("Draft", draft_count(),
        icon: "hero-pencil-square",
        color: :warning)
    ]
  end

  defp new_today, do: 0       # implement with real query
  defp published_count, do: 0 # implement with real query
  defp draft_count, do: 0     # implement with real query
end
```

`stat/2` and `stat/3` build stat card data. Options for `stat/3`:

| Option | Values | Description |
|--------|--------|-------------|
| `icon:` | Heroicon name | Icon displayed in the stat card |
| `color:` | `:success`, `:error`, `:warning`, `:info` | Value text color |
| `description:` | String | Subtitle text below the value |
| `chart:` | List of numbers | Renders an inline sparkline |

### Chart widget

Renders a Chart.js chart. Requires the hook setup described above.

```elixir
defmodule MyAppWeb.Admin.PostsChart do
  use PhoenixFilament.Widget.Chart

  @impl true
  def chart_type, do: :bar

  @impl true
  def chart_data(_assigns) do
    %{
      labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
      datasets: [
        %{
          label: "Posts Published",
          data: [12, 19, 8, 25, 14, 30],
          backgroundColor: "rgba(99, 102, 241, 0.5)"
        }
      ]
    }
  end

  # Optional: override default Chart.js options
  def chart_options do
    %{responsive: true, plugins: %{legend: %{position: "top"}}}
  end
end
```

Supported `chart_type` values: `:line`, `:bar`, `:pie`, `:doughnut`

### Table widget

Renders a read-only table on the dashboard.

```elixir
defmodule MyAppWeb.Admin.RecentPosts do
  use PhoenixFilament.Widget.Table

  @impl true
  def heading, do: "Recent Posts"

  @impl true
  def columns do
    [
      PhoenixFilament.Column.column(:title, label: "Title"),
      PhoenixFilament.Column.column(:inserted_at, label: "Date")
    ]
  end

  @impl true
  def update(assigns, socket) do
    {:ok, socket} = super(assigns, socket)
    rows = MyApp.Repo.all(
      from p in MyApp.Blog.Post,
        order_by: [desc: p.inserted_at],
        limit: 10
    )
    {:ok, Phoenix.Component.assign(socket, :rows, rows)}
  end
end
```

### Custom widget

For completely custom dashboard content, use `PhoenixFilament.Widget.Custom`:

```elixir
defmodule MyAppWeb.Admin.WelcomeWidget do
  use PhoenixFilament.Widget.Custom

  @impl true
  def render(assigns) do
    ~H"""
    <div class="card bg-base-100 shadow">
      <div class="card-body">
        <h2 class="card-title">Welcome to the Admin Panel</h2>
        <p>You are logged in as {@current_user.email}.</p>
      </div>
    </div>
    """
  end
end
```

### Registering widgets in your Panel

```elixir
defmodule MyAppWeb.Admin do
  use PhoenixFilament.Panel,
    path: "/admin",
    brand_name: "MyApp Admin"

  resources do
    resource MyAppWeb.Admin.PostResource, icon: "hero-document-text"
  end

  widgets do
    widget MyAppWeb.Admin.OverviewStats, sort: 1, column_span: :full
    widget MyAppWeb.Admin.PostsChart,    sort: 2, column_span: 6
    widget MyAppWeb.Admin.RecentPosts,   sort: 3, column_span: 6
  end
end
```

Widget options:

| Option | Default | Description |
|--------|---------|-------------|
| `sort:` | `0` | Rendering order (ascending) |
| `column_span:` | `12` (full width) | Grid column span: 1–12 or `:full` |

## Theming

PhoenixFilament uses daisyUI's theme system. Set a theme per panel:

```elixir
use PhoenixFilament.Panel,
  path: "/admin",
  brand_name: "MyApp Admin",
  theme: "corporate"
```

Popular daisyUI themes: `light`, `dark`, `corporate`, `retro`, `cyberpunk`, `cupcake`,
`bumblebee`, `emerald`, `synthwave`, `dracula`, `night`, `dim`, `nord`, `sunset`.

### Dark mode toggle

Enable a built-in light/dark toggle button in the panel header:

```elixir
use PhoenixFilament.Panel,
  path: "/admin",
  theme: "corporate",
  theme_switcher: true
```

When `theme_switcher: true`, a sun/moon icon button appears in the top navigation bar.
It uses daisyUI's `theme-controller` — clicking it toggles between the configured theme
and `dark`.

## Authentication

PhoenixFilament does not bundle an auth solution — it integrates with whatever your app
already uses.

### LiveView on_mount hook

The recommended approach is to use a LiveView `on_mount` hook. If you use `phx.gen.auth`,
the generated `UserAuth` module includes an `on_mount/4` callback:

```elixir
defmodule MyAppWeb.Admin do
  use PhoenixFilament.Panel,
    path: "/admin",
    on_mount: {MyAppWeb.UserAuth, :require_authenticated_user},
    brand_name: "MyApp Admin"

  resources do
    resource MyAppWeb.Admin.PostResource, icon: "hero-document-text"
  end
end
```

The `on_mount:` option must be a `{Module, :function}` tuple. The function must match the
Phoenix LiveView `on_mount` callback signature:

```elixir
def on_mount(:require_authenticated_user, _params, session, socket) do
  # validate session, assign current_user, or redirect
  {:cont, assign(socket, :current_user, user)}
  # or {:halt, redirect(socket, to: "/login")}
end
```

### HTTP-level authentication

For HTTP-level protection (guards against non-LiveView requests), use `pipe_through`:

```elixir
scope "/admin" do
  pipe_through [:browser, :require_authenticated_user]
  phoenix_filament_panel "/", MyAppWeb.Admin
end
```

### Session revocation

To disconnect all live sessions for a user (e.g. after password change, logout-everywhere):

```elixir
# In your UserAuth or session management code:
PhoenixFilament.Panel.revoke_sessions(MyApp.PubSub, current_user.id)
```

This requires the `pubsub:` option on your panel:

```elixir
use PhoenixFilament.Panel,
  path: "/admin",
  pubsub: MyApp.PubSub,
  on_mount: {MyAppWeb.UserAuth, :require_authenticated_user}
```

## Plugins

Plugins let you add custom navigation, routes, and widgets to a panel without modifying
the panel module directly.

### Using a community plugin

```elixir
defmodule MyAppWeb.Admin do
  use PhoenixFilament.Panel, path: "/admin"

  plugins do
    plugin MyApp.AnalyticsPlugin
    plugin MyApp.AuditLogPlugin, nav_group: "System"
  end
end
```

Plugin options (the keyword list after the module name) are passed to the plugin's
`register/2` callback.

### Creating a simple plugin

```elixir
defmodule MyApp.AnalyticsPlugin do
  use PhoenixFilament.Plugin

  @impl true
  def register(_panel, opts) do
    %{
      nav_items: [
        nav_item("Analytics",
          path: "/analytics",
          icon: "hero-chart-bar",
          nav_group: opts[:nav_group] || "Reports")
      ],
      routes: [
        route("/analytics", MyAppWeb.AnalyticsLive, :index)
      ]
    }
  end
end
```

See the [Plugin Development Guide](plugins.html) for the full plugin API.

## Authorization

You can define an `authorize/3` callback on any resource to control CRUD access:

```elixir
defmodule MyAppWeb.Admin.PostResource do
  use PhoenixFilament.Resource,
    schema: MyApp.Blog.Post,
    repo: MyApp.Repo

  def authorize(:delete, _record, %{role: "admin"}), do: :ok
  def authorize(:delete, _record, _user), do: {:error, :forbidden}
  def authorize(_action, _record, _user), do: :ok
end
```

`authorize/3` receives:
- `action` — `:index`, `:create`, `:edit`, `:delete`, or `:show`
- `record` — the Ecto struct (or `nil` for `:create`)
- `user` — the value of `socket.assigns.current_user`

Return `:ok` to allow or `{:error, reason}` to deny (raises `UnauthorizedError`).

## Next Steps

- [Resource Customization](resources.html) — Complete reference for form fields, table columns, filters, and authorization
- [Plugin Development](plugins.html) — Build and distribute your own plugins
- [Theming Guide](theming.html) — Custom themes, CSS variables, brand customization
- [API Reference](PhoenixFilament.Resource.html) — Module-level API documentation