For developers using PhoenixKit as a Hex dependency in their Phoenix application.
This guide is designed to help both developers and AI assistants (Claude, Cursor, Copilot, Tidewave MCP, etc.) understand how to integrate and use PhoenixKit effectively.
Table of Contents
Quick Start
# 1. Verify database is prepared
mix ecto.migrations
# 2. Install PhoenixKit (adds dep, fetches, configures, generates migrations)
mix igniter.install phoenix_kit
# 3. Create database if needed
mix ecto.create
# 4. Run migrations
mix ecto.migrate
# 5. Start your server
mix phx.server
# Visit /phoenix_kit/admin
Requires the
igniter_newarchive — install once withmix archive.install hex igniter_new. If you'd rather not, see the two-step fallback below.
Installation
Step 1: Run the Installer
mix igniter.install phoenix_kit
The installer will:
- Add
{:phoenix_kit, "~> 1.7"}to yourmix.exsand fetch deps - Detect your Repo automatically (or use
--repoto specify) - Add configuration to
config/config.exs - Generate migrations
- Set up mailer integration
Requires the
igniter_newarchive —mix archive.install hex igniter_newif you don't already have it.
Step 2: Install (two-step fallback)
If you can't or don't want to use the igniter_new archive, add the deps manually and run the installer directly. Declare both — PhoenixKit marks :igniter optional: true, and Mix does not resolve optional deps transitively, so a project that never declared igniter itself won't get it from PhoenixKit:
# mix.exs
defp deps do
[
{:phoenix_kit, "~> 1.7"},
{:igniter, "~> 0.7", only: [:dev, :test]}
]
endmix deps.get
mix phoenix_kit.install
Why optional? A stock mix phx.new app declares {:igniter, "~> 0.6", only: [:dev, :test]}. A non-optional dep in PhoenixKit resolves for all environments and Mix refuses to converge the two (the :only option for dependency igniter must include at least the environments of its parent), which broke mix igniter.install phoenix_kit on every freshly generated project.
Dev-only. only: [:dev, :test] is deliberate — igniter is build-time tooling that never reaches production. Every reference to it in PhoenixKit is inside the mix phoenix_kit.* tasks and their installer helpers; nothing in the supervision tree or request path touches it, and Mix tasks aren't part of a release.
If :igniter is missing, mix phoenix_kit.install and mix phoenix_kit.update print setup instructions rather than running. Everything that doesn't patch code — mix phoenix_kit.status, mix phoenix_kit.gen.migration, mix phoenix_kit.assets.rebuild — is unaffected.
Step 3: Configure
The installer adds this to your config. Customize as needed:
# config/config.exs
config :phoenix_kit,
parent_app_name: :my_app,
parent_module: MyApp,
url_prefix: "/phoenix_kit",
repo: MyApp.Repo,
mailer: MyApp.Mailer,
layouts_module: MyApp.Layouts,
phoenix_version_strategy: :modernStep 4: Add Routes
The installer adds something like this to your router.ex. Customize as needed:
# lib/my_app_web/router.ex
import PhoenixKitWeb.Integration
scope "/" do
pipe_through :browser
phoenix_kit_routes()
endStep 5: Run Migrations
mix ecto.migrate
Configuration Reference
Core Settings
| Setting | Type | Default | Description |
|---|---|---|---|
repo | module | auto-detected | Your Ecto Repo module |
mailer | module | nil | Your Swoosh Mailer module |
url_prefix | string | "/phoenix_kit" | URL prefix for all routes |
layout | tuple | PhoenixKit default | {LayoutModule, :template} |
root_layout | tuple | PhoenixKit default | Root layout for pages |
Authentication Settings
config :phoenix_kit, :password_requirements,
min_length: 8,
max_length: 72,
require_uppercase: false,
require_lowercase: false,
require_digit: false,
require_special: falseRate Limiting
# Hammer 7.x uses ETS backend via PhoenixKit.Users.RateLimiter.Backend
# No additional :hammer config needed
config :phoenix_kit, PhoenixKit.Users.RateLimiter,
login_limit: 5, # Max login attempts per window
login_window_ms: 60_000, # 1 minute window
magic_link_limit: 3, # Max magic link requests per window
magic_link_window_ms: 300_000, # 5 minute window
password_reset_limit: 3, # Max password reset requests per window
password_reset_window_ms: 300_000, # 5 minute window
registration_limit: 3, # Max registration attempts per window
registration_window_ms: 3_600_000, # 1 hour window
registration_ip_limit: 10, # Max registrations per IP per window
registration_ip_window_ms: 3_600_000 # 1 hour windowAuthentication Integration
Access Current User
# In a LiveView
def mount(_params, _session, socket) do
current_user = socket.assigns[:current_user]
{:ok, socket}
end
# In a Controller
def index(conn, _params) do
current_user = conn.assigns[:current_user]
render(conn, :index)
endRequire Authentication
# In your router
import PhoenixKitWeb.Users.Auth
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live "/dashboard", DashboardLive
endCheck User Roles
# Check if user has a role
PhoenixKit.Users.Roles.user_has_role?(user, "Admin")
PhoenixKit.Users.Roles.user_has_role?(user, "Owner")
# Get user's roles
roles = PhoenixKit.Users.Roles.get_user_roles(user)
# Check in templates
<%= if PhoenixKit.Users.Roles.user_has_role?(@current_user, "Admin") do %>
<.link navigate="/admin">Admin Panel</.link>
<% end %>Check Module-Level Permissions
PhoenixKit V53+ includes granular permissions that control which admin sections each role can access.
# In a LiveView, use the scope from assigns
scope = socket.assigns.phoenix_kit_current_scope
# Check single permission
Scope.has_module_access?(scope, "billing") # true/false
# Check multiple permissions
Scope.has_any_module_access?(scope, ["billing", "shop"]) # any granted?
Scope.has_all_module_access?(scope, ["billing", "shop"]) # all granted?
# Check system role (Owner or Admin, not custom roles)
Scope.system_role?(scope)
# Get all granted keys
Scope.accessible_modules(scope) # MapSet of granted permission keysRoute enforcement: PhoenixKit's phoenix_kit_ensure_admin and phoenix_kit_ensure_module_access on_mount hooks automatically enforce permissions on admin routes. Sidebar navigation is gated per-user.
User Registration
# Register a new user
{:ok, user} = PhoenixKit.Users.Auth.register_user(%{
email: "user@example.com",
password: "securepassword123"
})
# First user automatically becomes OwnerTroubleshooting
"Repo not configured"
# Ensure config is set
config :phoenix_kit, repo: MyApp.Repo"Routes not found"
# Ensure you imported and called the macro
import PhoenixKitWeb.Integration
phoenix_kit_routes()"Mailer not sending emails"
# Check your mailer is configured
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.SMTP,
# ... your SMTP settings
# And PhoenixKit knows about it
config :phoenix_kit, mailer: MyApp.Mailer"Rate limiting not working"
PhoenixKit uses Hammer 7.x with ETS backend via PhoenixKit.Users.RateLimiter.Backend. No additional Hammer configuration is required. If you need to adjust rate limits:
config :phoenix_kit, PhoenixKit.Users.RateLimiter,
login_limit: 5,
login_window_ms: 60_000mix phoenix_kit.install / mix phoenix_kit.update needs :igniter
Both tasks patch your code, so both require igniter — which PhoenixKit declares
optional: true and therefore does not hand down transitively. If it's
missing, the task prints setup instructions instead of running. Add it and
re-run:
{:igniter, "~> 0.7", only: [:dev, :test]}mix deps.get is enough — PhoenixKit tracks whether igniter was available when
it last compiled and recompiles itself when that changes, so the tasks appear
without a manual mix deps.compile phoenix_kit --force.
This bites most often on upgrade: a project that never declared igniter was
getting it transitively from an older PhoenixKit, and the switch to optional: true drops it.
mix igniter.install crashes with Igniter.CopiedTasks is not available
Not a PhoenixKit failure. When your mix.exs has no {:igniter, ...} line, the
igniter_new archive temporarily injects one, shells out to mix deps.get,
then removes it on exit. If that deps.get fails, the archive's cleanup handler
crashes on top of the original error because igniter was never loaded — burying
the real message. Read the mix deps.get failed with exit code ... output above
that stack trace, and check mix.exs for a stray {:igniter, "~> 0.6", only: [:dev, :test]} line the crashed cleanup left behind.
Declaring igniter permanently (see above) avoids the inject/cleanup dance
entirely — igniter_new skips it when the dep is already present.
Further Reading
- Custom Admin Pages - Add pages to the admin sidebar
- Admin Dashboard Reference - Admin navigation and tabs system
- Dashboard Components - Tabs, subtabs, badges, and more
For AI Assistants
When helping a developer with PhoenixKit:
- PhoenixKit is a Hex dependency - Code lives in
deps/phoenix_kit/ - Don't modify PhoenixKit files - Create code in the user's app that calls PhoenixKit APIs
- Entity names are snake_case - e.g.,
"contact_form", not"Contact Form" - Field keys are snake_case - e.g.,
"full_name", not"Full Name" - First user is Owner - First registered user gets the Owner role automatically
- Routes are prefixed - Default is
/phoenix_kit/, configurable viaurl_prefix - Permissions are cached in Scope - Use
Scope.has_module_access?/2not raw DB queries - Owner bypasses all permission checks - No DB rows needed for Owner access
- Entities use
created_by_uuid- The field iscreated_by_uuid, notcreated_by - Always use
entity.uuid- Never useentity.idfor entity operations
Last Updated: 2026-03-02