A minimal PhoenixKit plugin module — use this as a starting point for your own.
This module demonstrates every required and commonly-used optional callback from
the PhoenixKit.Module behaviour. Copy this project, rename it, and replace the
callbacks with your own logic.
How it works
use PhoenixKit.Modulemarks this module as a plugin (persists a@phoenix_kit_moduleattribute in the.beamfile).- PhoenixKit scans
.beamfiles at startup and discovers this module automatically — no config line needed. - The callbacks below tell PhoenixKit how to integrate the module: admin tabs, permissions, enable/disable toggling, etc.
Installation
Add to your parent app's mix.exs:
{:phoenix_kit_hello_world, "~> 0.2"}Or for local development:
{:phoenix_kit_hello_world, path: "../phoenix_kit_hello_world"}Then run mix deps.get. That's it — the module appears in the admin
Modules page and sidebar automatically.
Database
This module has no tables — it's a template, and a demo module has no
business creating one in every host that installs it. When YOUR module needs
tables, it ships the migrations that create them: a versioned coordinator in
your own repo, returned from migration_module/0, never a new Vxxx in core
phoenix_kit's chain. mix phoenix_kit.update in the host app then installs
and upgrades them, so there is no install task to write. The copyable
(all-comments) template is lib/phoenix_kit_hello_world/migrations.ex.
What you get for free
- Admin sidebar tab (appears/disappears when module is toggled)
- Entry in the admin Modules page with enable/disable toggle
- Permission key in the roles/permissions matrix
- Live sidebar updates (no page reload needed when toggling)
- Route auto-generated at compile time from the
live_viewfield
Navigation paths
All href attributes and redirect/2 calls must go through
PhoenixKit.Utils.Routes.path/1 — never use relative paths.
Create a Paths module (e.g., MyModule.Paths) that wraps
Routes.path/1 to centralize your module's paths in one place.
See the README for the full pattern.
JavaScript
External modules cannot inject into the parent app's JS build pipeline.
All JavaScript must be inline <script> tags in your templates.
Register hooks on window.PhoenixKitHooks — PhoenixKit spreads this
into the LiveSocket automatically. See the README for full details.
Callbacks overview
| Callback | Required? | What it does |
|---|---|---|
module_key/0 | Yes | Unique string key (used in settings, permissions) |
module_name/0 | Yes | Human-readable name (shown in admin UI) |
enabled?/0 | Yes | Whether the module is currently on |
enable_system/0 | Yes | Turn the module on (persists to DB) |
disable_system/0 | Yes | Turn the module off (persists to DB) |
permission_metadata/0 | No | Icon, label, description for permissions UI |
admin_tabs/0 | No | Tabs to add to the admin sidebar |
settings_tabs/0 | No | Tabs to add to the admin settings page |
children/0 | No | Supervisor child specs (GenServers, workers, etc.) |
version/0 | No | Version string (default: "0.0.0") |
get_config/0 | No | Stats/config map shown on the Modules page |
route_module/0 | No | Module providing custom route macros |
user_dashboard_tabs/0 | No | Tabs for the user-facing dashboard |
migration_module/0 | No | Versioned migration coordinator module |
required_integrations/0 | No | Integration provider keys this module needs |
integration_providers/0 | No | Custom provider definitions to contribute |
Summary
Functions
Admin sidebar tabs for this module.
OTP apps whose templates Tailwind should scan for CSS classes.
Disables the module. Same pattern as enable_system/0.
Enables the module by persisting a boolean setting.
Whether the module is currently enabled.
Unique key for this module. Used in settings, permissions, and PubSub events.
Display name shown in the admin UI.
Notification types this module contributes.
Permission metadata for the roles/permissions matrix.
Optional integration with phoenix_kit_comments: make this module's resources
clickable in the comments moderation admin.
Version string. Shown on the admin Modules page.
Functions
Admin sidebar tabs for this module.
Each tab needs at minimum: :id, :label, :path, :level, :permission.
Key fields:
:id— unique atom across ALL modules (prefix with:admin_yourmodule):path— must start with/adminand use hyphens, not underscores:permission— must matchmodule_key/0so custom roles get proper access:group— use:admin_modulesto appear in the Modules section of the sidebar:priority— controls sort order (higher = further down). Built-in modules use 500-620; use 640+ for external modules:live_view—{Module, :action}tuple; PhoenixKit auto-generates the route:icon— Heroicon name (optional, shown in sidebar):match—:exactor:prefixfor active-state highlighting
Return [] to have no admin tabs (default).
OTP apps whose templates Tailwind should scan for CSS classes.
Disables the module. Same pattern as enable_system/0.
Enables the module by persisting a boolean setting.
update_boolean_setting_with_module/3 stores the value and tracks which
module owns the setting. The third argument must match module_key/0.
Whether the module is currently enabled.
Reads from the DB-backed settings table. Defensive against three failure modes that can hit before/around DB availability:
rescue _: DB not running, table missing, schema mismatch, etc.catch :exit, _: connection pool checkoutEXIT(e.g. when a test sandbox owner has just stopped — test-environment artifact, but harmless to handle in production code too).
All branches return false so callers don't need to special-case
startup ordering.
Unique key for this module. Used in settings, permissions, and PubSub events.
Display name shown in the admin UI.
Notification types this module contributes.
Each type becomes a per-user toggle in notification preferences. actions
maps the activity action strings this module emits to the type, so a user
who mutes "Hello World" stops receiving those notifications.
Optionally declare sub_types for finer control: the base type then acts as a
master switch (off ⇒ every sub muted) and each sub is individually toggleable
(see PhoenixKit.Notifications.Types). Sub keys are bare here ("greetings")
and composed to "hello_world.greetings" at registration. When a type is fully
split, leave its own actions: [] so every action is owned by exactly one sub.
See the Notifications admin page (Web.NotificationsLive) for the senders.
Permission metadata for the roles/permissions matrix.
The :key MUST match module_key/0 — PhoenixKit validates this at startup.
Icons use the hero- prefix (Heroicons via phoenix_heroicons).
Return nil to opt out of the permissions system entirely (default).
Optional integration with phoenix_kit_comments: make this module's resources
clickable in the comments moderation admin.
If your module owns things that users comment on (the resource_type you pass
to the comments component), implement resolve_comment_resources/1 to turn a
list of resource uuids into display chips. Return %{uuid => info} where
info is:
:title— the chip label (e.g. the record's name):path— a raw app path, e.g."/admin/widgets/#{uuid}". The comments module runs it throughPhoenixKit.Utils.Routes.path/1itself (prefix + locale), so do NOT pre-apply the prefix here or the link double-prefixes.:thumb_url— optional image URL for a thumbnail (otherwise a type badge shows). Omit the key when there's none.
Then register the handler so comments dispatches "hello_world" resources to
this module:
# config/config.exs
config :phoenix_kit, :comment_resource_handlers, %{
"hello_world" => PhoenixKitHelloWorld
}A real implementation queries your schema:
def resolve_comment_resources(uuids) do
import Ecto.Query
from(w in Widget, where: w.uuid in ^uuids, select: {w.uuid, w.name})
|> Repo.all()
|> Map.new(fn {uuid, name} ->
{uuid, %{title: name, path: "/admin/widgets/#{uuid}"}}
end)
rescue
_ -> %{}
endHello World has no resources of its own, so this returns an empty map.
Hosts can also link a type with no code via Settings → Comments →
Resource Paths (a path template like /admin/widgets/:uuid).
Version string. Shown on the admin Modules page.