Experimental
The Plugin API is @experimental. Breaking changes may occur in minor versions until
this notice is removed. Pin your phoenix_filament dependency to a specific version
when building plugins.
Plugins let you extend a PhoenixFilament panel with custom navigation, live routes, dashboard widgets, and lifecycle hooks — using the exact same API that PhoenixFilament uses internally.
Quick Start
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: "Reports")
],
routes: [
route("/analytics", MyAppWeb.AnalyticsLive, :index)
]
}
end
endRegister it in your panel:
defmodule MyAppWeb.Admin do
use PhoenixFilament.Panel, path: "/admin"
plugins do
plugin MyApp.AnalyticsPlugin
end
enduse PhoenixFilament.Plugin
Using this macro:
- Adds
@behaviour PhoenixFilament.Pluginto your module - Imports
nav_item/2androute/3helper functions
Callbacks
register/2 (required)
Called at compile time when the panel resolves its plugin list. Returns a map describing what the plugin contributes to the panel.
@impl true
def register(panel_module, opts) do
%{
nav_items: [...],
routes: [...],
widgets: [...],
hooks: [...]
}
endArguments:
panel_module— the Panel module that is registering this pluginopts— the keyword list passed toplugin MyPlugin, key: value
All keys in the returned map are optional. Omit any key you do not need.
:nav_items
Navigation entries added to the panel sidebar. Build them with nav_item/2:
nav_item("Analytics",
path: "/analytics",
icon: "hero-chart-bar",
nav_group: "Reports")nav_item/2 options:
| Key | Type | Description |
|---|---|---|
path: | string | URL path (relative to panel root) |
icon: | string | Heroicon name |
nav_group: | string | Sidebar group heading |
:routes
Live routes added to the panel's live_session. Build them with route/3:
route("/analytics", MyAppWeb.AnalyticsLive, :index)
route("/analytics/:id", MyAppWeb.AnalyticsLive, :show)route/3 arguments:
- Path string (relative to the panel's
scopepath) - LiveView module
- Live action atom
All routes registered via plugins automatically inherit the panel's on_mount hooks,
session name, and layout.
:widgets
Dashboard widgets contributed by the plugin:
widgets: [
%{module: MyApp.AnalyticsWidget, sort: 5, column_span: 6}
]Widget map keys:
| Key | Default | Description |
|---|---|---|
module | required | LiveComponent module |
sort | 0 | Dashboard rendering order (ascending) |
column_span | 12 | Grid column span (1–12) |
The widget module must implement one of the widget behaviours:
PhoenixFilament.Widget.StatsOverview, PhoenixFilament.Widget.Chart,
PhoenixFilament.Widget.Table, or PhoenixFilament.Widget.Custom.
:hooks
Lifecycle hooks called at various points in the panel LiveView:
hooks: [
{:handle_event, &MyApp.AnalyticsPlugin.handle_event/3},
{:handle_info, &MyApp.AnalyticsPlugin.handle_info/2},
{:handle_params, &MyApp.AnalyticsPlugin.handle_params/3},
{:after_render, &MyApp.AnalyticsPlugin.after_render/1}
]Hook function signatures:
# handle_event: called before the panel's handle_event
def handle_event(event, params, socket), do: {:cont, socket}
# handle_info: called before the panel's handle_info
def handle_info(message, socket), do: {:cont, socket}
# handle_params: called before the panel's handle_params
def handle_params(params, uri, socket), do: {:cont, socket}
# after_render: called after each render
def after_render(socket), do: socketReturn {:cont, socket} to allow the default handler to proceed, or {:halt, socket}
to stop further processing.
boot/1 (optional)
Called at runtime on each panel LiveView mount. Receives the socket after the panel's
own on_mount hooks have run. Returns the modified socket.
@impl true
def boot(socket) do
user_id = socket.assigns.current_user.id
# Subscribe to a PubSub topic
Phoenix.PubSub.subscribe(MyApp.PubSub, "analytics:#{user_id}")
# Add assigns available throughout the panel session
Phoenix.Component.assign(socket, :analytics_enabled, true)
endboot/1 cannot halt the mount — authentication is the Panel's responsibility.
Plugin Options
Pass configuration to your plugin from the panel:
plugins do
plugin MyApp.AnalyticsPlugin,
nav_group: "Reports",
show_realtime: true
endAccess options in register/2:
def register(_panel, opts) do
group = opts[:nav_group] || "Analytics"
show_realtime = opts[:show_realtime] || false
%{
nav_items: [
nav_item("Analytics", path: "/analytics", nav_group: group)
] ++ if(show_realtime, do: [nav_item("Live", path: "/analytics/live", nav_group: group)], else: [])
}
endComplete Plugin Example
defmodule MyApp.AuditLogPlugin do
use PhoenixFilament.Plugin
@impl true
def register(_panel, opts) do
group = opts[:nav_group] || "System"
%{
nav_items: [
nav_item("Audit Log",
path: "/audit",
icon: "hero-clipboard-document-list",
nav_group: group)
],
routes: [
route("/audit", MyAppWeb.AuditLive, :index),
route("/audit/:id", MyAppWeb.AuditLive, :show)
],
widgets: [
%{module: MyApp.RecentAuditWidget, sort: 10, column_span: 12}
],
hooks: [
{:handle_info, &__MODULE__.handle_info/2}
]
}
end
@impl true
def boot(socket) do
if Map.has_key?(socket.assigns, :current_user) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "audit_log")
end
socket
end
def handle_info({:audit_event, event}, socket) do
# Update live audit count in sidebar badge, etc.
updated = update_in(socket.assigns[:audit_count] || 0, &(&1 + 1))
{:cont, Phoenix.Component.assign(socket, :audit_count, updated)}
end
def handle_info(_msg, socket), do: {:cont, socket}
endTesting Your Plugin
Use ExUnit with use ExUnit.Case:
defmodule MyApp.AuditLogPluginTest do
use ExUnit.Case, async: true
describe "register/2" do
test "returns nav_items and routes" do
result = MyApp.AuditLogPlugin.register(MyAppWeb.Admin, [])
assert [nav_item] = result.nav_items
assert nav_item.label == "Audit Log"
assert nav_item.path == "/audit"
assert [route1, route2] = result.routes
assert route1.path == "/audit"
assert route1.live_action == :index
end
test "respects nav_group option" do
result = MyApp.AuditLogPlugin.register(MyAppWeb.Admin, nav_group: "Security")
[nav_item] = result.nav_items
assert nav_item.nav_group == "Security"
end
end
describe "boot/1" do
test "assigns analytics_enabled" do
socket = %Phoenix.LiveView.Socket{assigns: %{current_user: %{id: 1}}}
result = MyApp.AuditLogPlugin.boot(socket)
# boot modifies the socket — assert your expected changes
assert result != nil
end
end
endStability Contract
| Version | Status |
|---|---|
| v0.1.x | @experimental — breaking changes possible in minor versions |
| v0.2+ | Stabilize based on community feedback |
| v1.0 | Stable, semver-protected |
Pin your dependency to a patch version while the API is experimental:
{:phoenix_filament, "~> 0.1.0"}