PhoenixKit Integration Guide

Copy Markdown View Source

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

  1. Quick Start
  2. Installation
  3. Configuration Reference
  4. Using the Entities System
  5. Public Forms
  6. Authentication Integration
  7. Common Tasks
  8. API Reference
  9. Troubleshooting

Quick Start

# 1. Add to mix.exs
{:phoenix_kit, "~> 1.6"}

# 2. Run installation
mix deps.get
mix phoenix_kit.install

# 3. Run migrations
mix ecto.migrate

# 4. Add routes to your router.ex
import PhoenixKitWeb.Integration
phoenix_kit_routes()

# 5. Start your server
mix phx.server
# Visit /phoenix_kit/admin

Installation

Step 1: Add Dependency

# mix.exs
defp deps do
  [
    {:phoenix_kit, "~> 1.6"}
  ]
end

Step 2: Install

mix deps.get
mix phoenix_kit.install --repo MyApp.Repo

The installer will:

  • Detect your Repo automatically (or use --repo to specify)
  • Add configuration to config/config.exs
  • Generate migrations
  • Set up mailer integration

Step 3: Configure

The installer adds this to your config. Customize as needed:

# config/config.exs
config :phoenix_kit,
  repo: MyApp.Repo,
  mailer: MyApp.Mailer,  # Uses your app's mailer
  url_prefix: "/phoenix_kit"  # URL prefix for all routes

# Optional: Use your app's layouts
config :phoenix_kit,
  layout: {MyAppWeb.Layouts, :app},
  root_layout: {MyAppWeb.Layouts, :root}

Step 4: Add Routes

# lib/my_app_web/router.ex
import PhoenixKitWeb.Integration

scope "/" do
  pipe_through :browser
  phoenix_kit_routes()
end

Step 5: Run Migrations

mix ecto.migrate

Configuration Reference

Core Settings

SettingTypeDefaultDescription
repomoduleauto-detectedYour Ecto Repo module
mailermodulenilYour Swoosh Mailer module
url_prefixstring"/phoenix_kit"URL prefix for all routes
layouttuplePhoenixKit default{LayoutModule, :template}
root_layouttuplePhoenixKit defaultRoot 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: false

Rate Limiting

config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000, cleanup_interval_ms: 60_000]}

config :phoenix_kit, PhoenixKit.Users.RateLimiter,
  login_limit: 5,
  login_window_ms: 60_000,
  magic_link_limit: 3,
  magic_link_window_ms: 300_000

Using the Entities System

The Entities system lets you create custom content types without database migrations.

Enable the System

# Via code
PhoenixKit.Entities.enable_system()

# Or via admin UI at /phoenix_kit/admin/modules

Create an Entity Programmatically

{:ok, entity} = PhoenixKit.Entities.create_entity(%{
  name: "contact_form",
  display_name: "Contact Form",
  description: "Contact form submissions",
  icon: "hero-envelope",
  status: "published",
  created_by: admin_user.id,
  fields_definition: [
    %{
      "type" => "text",
      "key" => "name",
      "label" => "Full Name",
      "required" => true
    },
    %{
      "type" => "email",
      "key" => "email",
      "label" => "Email Address",
      "required" => true
    },
    %{
      "type" => "textarea",
      "key" => "message",
      "label" => "Message",
      "required" => true
    }
  ]
})

Create Data Records

{:ok, record} = PhoenixKit.Entities.EntityData.create(%{
  entity_id: entity.id,
  title: "New Contact",
  status: "published",
  created_by: user.id,
  data: %{
    "name" => "John Doe",
    "email" => "john@example.com",
    "message" => "Hello!"
  }
})

Query Records

# All records for an entity
records = PhoenixKit.Entities.EntityData.list_by_entity(entity.id)

# Search by title (search_term first, entity_id optional second)
results = PhoenixKit.Entities.EntityData.search_by_title("John", entity.id)

# Get entity by name
entity = PhoenixKit.Entities.get_entity_by_name("contact_form")

Available Field Types

TypeDescriptionRequires OptionsStatus
textSingle-line textNo
textareaMulti-line textNo
emailEmail with validationNo
urlURL with validationNo
numberNumeric inputNo
booleanTrue/false toggleNo
dateDate pickerNo
rich_textWYSIWYG editorNo
selectDropdownYes
radioRadio buttonsYes
checkboxMultiple checkboxesYes
imageImage uploadNo🚧 Coming soon
fileFile uploadNo🚧 Coming soon
relationLink to other entityYes🚧 Coming soon

Note: Media and relation fields are defined in the schema but render "Coming Soon" placeholders in forms. No actual upload or relation functionality is implemented yet.

Field Builder Helpers

Use these helpers to create field definitions more easily:

alias PhoenixKit.Entities.FieldTypes

# Text fields
FieldTypes.text_field("name", "Full Name", required: true)
FieldTypes.textarea_field("bio", "Biography")
FieldTypes.email_field("email", "Email Address", required: true)
FieldTypes.rich_text_field("content", "Content")

# Numeric and boolean
FieldTypes.number_field("age", "Age")
FieldTypes.boolean_field("active", "Is Active", default: true)

# Choice fields with options
FieldTypes.select_field("category", "Category", ["Tech", "Business", "Other"])
FieldTypes.radio_field("priority", "Priority", ["Low", "Medium", "High"], required: true)
FieldTypes.checkbox_field("tags", "Tags", ["Featured", "Popular", "New"])

# Generic with options
FieldTypes.new_field("select", "status", "Status", options: ["Active", "Inactive"], required: true)

Creating Entity with Choice Fields

alias PhoenixKit.Entities
alias PhoenixKit.Entities.FieldTypes

# Note: created_by is optional - it auto-fills with first admin user if not provided
{:ok, entity} = Entities.create_entity(%{
  name: "contact_form",
  display_name: "Contact Form",
  status: "published",
  # created_by: admin.id,  # Optional! Auto-filled if omitted
  fields_definition: [
    FieldTypes.text_field("name", "Name", required: true),
    FieldTypes.email_field("email", "Email", required: true),
    FieldTypes.select_field("subject", "Subject", [
      "General Inquiry",
      "Support",
      "Sales",
      "Partnership"
    ], required: true),
    FieldTypes.textarea_field("message", "Message", required: true),
    FieldTypes.checkbox_field("interests", "Interests", [
      "Product Updates",
      "Newsletter",
      "Events"
    ])
  ]
})

Getting Admin User for created_by

If you need to explicitly set created_by, use these helpers:

# Get first admin (Owner or Admin role) - recommended
admin_id = PhoenixKit.Users.Auth.get_first_admin_id()

# Get first user (any role)
user_id = PhoenixKit.Users.Auth.get_first_user_id()

# Get full user struct if needed
admin = PhoenixKit.Users.Auth.get_first_admin()

Note: created_by is now auto-filled for both Entities.create_entity/1 and EntityData.create/1 if not provided. It uses the first admin, or falls back to the first user.


Public Forms

Embed entity-based forms on public pages for contact forms, surveys, lead capture, etc.

Enable Public Form for an Entity

# Via admin UI: /phoenix_kit/admin/entities/:id/edit
# Or programmatically:
PhoenixKit.Entities.update_entity(entity, %{
  settings: %{
    "public_form_enabled" => true,
    "public_form_fields" => ["name", "email", "message"],
    "public_form_title" => "Contact Us",
    "public_form_description" => "We'll get back to you within 24 hours.",
    "public_form_submit_text" => "Send Message",
    "public_form_success_message" => "Thank you! We received your message."
  }
})

Embed in Your Templates

The EntityForm is a function component (not a LiveComponent), so use it directly:

<%# In .phk publishing pages (recommended) %>
<EntityForm entity_slug="contact_form" />

<%# Or call the render function directly in regular .heex templates %>
<PhoenixKit.Modules.Publishing.Components.EntityForm.render
  attributes={%{"entity_slug" => "contact_form"}}
/>

Note: Do not use live_component - EntityForm uses Phoenix.Component, not Phoenix.LiveComponent.

Security Options

Configure in entity settings or admin UI:

SettingDefaultDescription
public_form_honeypotfalseHidden field to catch bots
public_form_time_checkfalseReject submissions < 3 seconds
public_form_rate_limitfalse5 submissions/minute per IP
public_form_debug_modefalseShow detailed error messages
public_form_collect_metadatatrueCapture IP, browser, device

Security Actions

Each security check can be configured with an action:

ActionBehavior
reject_silentShow fake success, don't save
reject_errorShow error message, don't save
save_suspiciousSave with "draft" status, flag in metadata
save_logSave normally, log warning

Form Submission Route

Forms POST to: POST /phoenix_kit/entities/:entity_slug/submit

This is handled by PhoenixKitWeb.EntityFormController.


Authentication 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)
end

Require Authentication

# In your router
import PhoenixKitWeb.Users.Auth

scope "/", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user]

  live "/dashboard", DashboardLive
end

Check User Roles

# Check if user has a role
PhoenixKit.Users.Roles.has_role?(user, "admin")
PhoenixKit.Users.Roles.has_role?(user, "owner")

# Get user's roles
roles = PhoenixKit.Users.Roles.list_user_roles(user.id)

# Check in templates
<%= if PhoenixKit.Users.Roles.has_role?(@current_user, "admin") do %>
  <.link navigate="/admin">Admin Panel</.link>
<% end %>

User Registration

# Register a new user
{:ok, user} = PhoenixKit.Users.Auth.register_user(%{
  email: "user@example.com",
  password: "securepassword123"
})

# First user automatically becomes Owner

Common Tasks

Task: Add PhoenixKit Navigation to Your Layout

<%# In your app's layout %>
<nav>
  <%= if @current_user do %>
    <.link navigate="/phoenix_kit/admin">Admin</.link>
    <.link href="/phoenix_kit/users/log_out" method="delete">Log out</.link>
  <% else %>
    <.link navigate="/phoenix_kit/users/log_in">Log in</.link>
  <% end %>
</nav>

Task: Create a Contact Form Entity

# In a migration or seeds.exs
admin = PhoenixKit.Users.Auth.get_user_by_email("admin@example.com")

{:ok, _entity} = PhoenixKit.Entities.create_entity(%{
  name: "contact",
  display_name: "Contact Submission",
  status: "published",
  created_by: admin.id,
  fields_definition: [
    %{"type" => "text", "key" => "name", "label" => "Name", "required" => true},
    %{"type" => "email", "key" => "email", "label" => "Email", "required" => true},
    %{"type" => "select", "key" => "subject", "label" => "Subject", "required" => true,
      "options" => ["General Inquiry", "Support", "Sales", "Partnership"]},
    %{"type" => "textarea", "key" => "message", "label" => "Message", "required" => true}
  ],
  settings: %{
    "public_form_enabled" => true,
    "public_form_fields" => ["name", "email", "subject", "message"],
    "public_form_title" => "Contact Us",
    "public_form_honeypot" => true,
    "public_form_time_check" => true,
    "public_form_rate_limit" => true
  }
})

Task: List All Contact Submissions

entity = PhoenixKit.Entities.get_entity_by_name("contact")
submissions = PhoenixKit.Entities.EntityData.list_by_entity(entity.id)

for submission <- submissions do
  IO.puts("#{submission.data["name"]} - #{submission.data["email"]}")
end

Task: Export Entity Data

entity = PhoenixKit.Entities.get_entity_by_name("contact")
records = PhoenixKit.Entities.EntityData.list_by_entity(entity.id)

# Convert to list of maps
data = Enum.map(records, fn r ->
  Map.merge(r.data, %{
    "id" => r.id,
    "created_at" => r.date_created,
    "status" => r.status
  })
end)

# Export as JSON
Jason.encode!(data)

API Reference

PhoenixKit.Entities

# Check if system is enabled
PhoenixKit.Entities.enabled?() :: boolean()

# Enable/disable
PhoenixKit.Entities.enable_system() :: {:ok, Setting.t()}
PhoenixKit.Entities.disable_system() :: {:ok, Setting.t()}

# Get by ID
PhoenixKit.Entities.get_entity(id) :: Entity.t() | nil        # Returns nil if not found
PhoenixKit.Entities.get_entity!(id) :: Entity.t()             # Raises if not found
PhoenixKit.Entities.get_entity_by_name(name) :: Entity.t() | nil

# List
PhoenixKit.Entities.list_entities() :: [Entity.t()]
PhoenixKit.Entities.list_active_entities() :: [Entity.t()]    # Only status: "published"

# Create/Update/Delete
PhoenixKit.Entities.create_entity(attrs) :: {:ok, Entity.t()} | {:error, Changeset.t()}
PhoenixKit.Entities.update_entity(entity, attrs) :: {:ok, Entity.t()} | {:error, Changeset.t()}
PhoenixKit.Entities.delete_entity(entity) :: {:ok, Entity.t()} | {:error, Changeset.t()}

# Changeset (for forms)
PhoenixKit.Entities.change_entity(entity, attrs \\ %{}) :: Changeset.t()

# Stats
PhoenixKit.Entities.get_system_stats() :: %{
  total_entities: integer(),
  active_entities: integer(),
  total_data_records: integer()
}

PhoenixKit.Entities.EntityData

# Get by ID
EntityData.get(id) :: EntityData.t() | nil           # Returns nil if not found
EntityData.get!(id) :: EntityData.t()                # Raises if not found
EntityData.get_by_slug(entity_id, slug) :: EntityData.t() | nil

# List/Query
EntityData.list_all() :: [EntityData.t()]
EntityData.list_by_entity(entity_id) :: [EntityData.t()]
EntityData.list_by_entity_and_status(entity_id, status) :: [EntityData.t()]
EntityData.search_by_title(search_term, entity_id \\ nil) :: [EntityData.t()]

# Create/Update/Delete
EntityData.create(attrs) :: {:ok, EntityData.t()} | {:error, Changeset.t()}
EntityData.update(record, attrs) :: {:ok, EntityData.t()} | {:error, Changeset.t()}
EntityData.delete(record) :: {:ok, EntityData.t()} | {:error, Changeset.t()}

# Changeset (for forms)
EntityData.change(record, attrs \\ %{}) :: Changeset.t()

PhoenixKit.Entities.FieldTypes

# Field builder helpers (recommended for programmatic entity creation)
FieldTypes.text_field(key, label, opts \\ []) :: map()
FieldTypes.textarea_field(key, label, opts \\ []) :: map()
FieldTypes.email_field(key, label, opts \\ []) :: map()
FieldTypes.number_field(key, label, opts \\ []) :: map()
FieldTypes.boolean_field(key, label, opts \\ []) :: map()
FieldTypes.rich_text_field(key, label, opts \\ []) :: map()

# Choice field helpers (options required)
FieldTypes.select_field(key, label, options, opts \\ []) :: map()
FieldTypes.radio_field(key, label, options, opts \\ []) :: map()
FieldTypes.checkbox_field(key, label, options, opts \\ []) :: map()

# Generic field builder
FieldTypes.new_field(type, key, label, opts \\ []) :: map()
# opts: [required: bool, default: any, options: list]

# Field type info
FieldTypes.all() :: map()
FieldTypes.requires_options?(type) :: boolean()
FieldTypes.validate_field(field_map) :: {:ok, map()} | {:error, String.t()}

PhoenixKit.Users.Auth

# User management
Auth.get_user(id) :: User.t() | nil
Auth.get_user!(id) :: User.t()
Auth.get_user_by_email(email) :: User.t() | nil
Auth.register_user(attrs) :: {:ok, User.t()} | {:error, Changeset.t()}

# Admin user helpers (useful for created_by)
Auth.get_first_admin() :: User.t() | nil       # First Owner or Admin
Auth.get_first_admin_id() :: integer() | nil   # Just the ID
Auth.get_first_user() :: User.t() | nil        # First user by ID
Auth.get_first_user_id() :: integer() | nil    # Just the ID

# Authentication
Auth.authenticate_user(email, password) :: {:ok, User.t()} | {:error, :invalid_credentials}

# Session
Auth.generate_user_session_token(user) :: binary()
Auth.get_user_by_session_token(token) :: User.t() | nil
Auth.delete_user_session_token(token) :: :ok

PhoenixKit.Users.Roles

Roles.has_role?(user, role_name) :: boolean()
Roles.list_user_roles(user_id) :: [Role.t()]
Roles.assign_role(user_id, role_name, assigned_by) :: {:ok, RoleAssignment.t()} | {:error, term()}
Roles.remove_role(user_id, role_name) :: :ok | {:error, term()}

PhoenixKit.Settings

Settings.get(key) :: String.t() | nil
Settings.get(key, default) :: String.t()
Settings.set(key, value) :: {:ok, Setting.t()}
Settings.get_boolean(key) :: boolean()
Settings.get_integer(key) :: integer()

Troubleshooting

"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()

"Entities menu not showing"

The Entities module must be enabled:

PhoenixKit.Entities.enable_system()
# Or visit /phoenix_kit/admin/modules and enable it

"Public form shows 'unavailable'"

Check that:

  1. Entity status is "published"
  2. public_form_enabled is true
  3. public_form_fields has at least one field

"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"

Ensure Hammer is configured:

config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000, cleanup_interval_ms: 60_000]}

File Locations

When working with PhoenixKit source (for debugging or understanding):

PurposeLocation
Entities core logicdeps/phoenix_kit/lib/phoenix_kit/entities/
Entity data schemadeps/phoenix_kit/lib/phoenix_kit/entities/entity_data.ex
Field typesdeps/phoenix_kit/lib/phoenix_kit/entities/field_types.ex
Public form controllerdeps/phoenix_kit/lib/phoenix_kit_web/controllers/entity_form_controller.ex
Public form componentdeps/phoenix_kit/lib/modules/publishing/components/entity_form.ex
Authenticationdeps/phoenix_kit/lib/phoenix_kit/users/auth.ex
User schemadeps/phoenix_kit/lib/phoenix_kit/users/auth/user.ex
Rolesdeps/phoenix_kit/lib/phoenix_kit/users/roles.ex
Settingsdeps/phoenix_kit/lib/phoenix_kit/settings.ex
Router integrationdeps/phoenix_kit/lib/phoenix_kit_web/integration.ex

For AI Assistants

When helping a developer with PhoenixKit:

  1. PhoenixKit is a Hex dependency - Code lives in deps/phoenix_kit/
  2. Don't modify PhoenixKit files - Create code in the user's app that calls PhoenixKit APIs
  3. Check if Entities is enabled - PhoenixKit.Entities.enabled?()
  4. Entity names are snake_case - e.g., "contact_form", not "Contact Form"
  5. Field keys are snake_case - e.g., "full_name", not "Full Name"
  6. Public forms need fields selected - Both public_form_enabled and public_form_fields must be set
  7. First user is Owner - First registered user gets the Owner role automatically
  8. Routes are prefixed - Default is /phoenix_kit/, configurable via url_prefix

Common Patterns

# Get current user in LiveView
@current_user = socket.assigns[:current_user]

# Check admin access
if PhoenixKit.Users.Roles.has_role?(user, "admin"), do: ...

# Create entity with public form
PhoenixKit.Entities.create_entity(%{
  name: "...",
  fields_definition: [...],
  settings: %{"public_form_enabled" => true, "public_form_fields" => [...]}
})

# Query submissions
entity = PhoenixKit.Entities.get_entity_by_name("contact")
records = PhoenixKit.Entities.EntityData.list_by_entity(entity.id)

Last Updated: 2025-12-03