PhoenixKitEntities

Dynamic content types for PhoenixKit. Define custom entities (like "Product", "Team Member", "FAQ") with flexible field schemas — no database migrations needed per entity.

Table of Contents

What this provides

Quick start

Add to your parent app's mix.exs:

{:phoenix_kit_entities, path: "../phoenix_kit_entities"}

Run mix deps.get and start the server. The module appears in:

Enable the system:

PhoenixKitEntities.enable_system()

Create your first entity:

{:ok, entity} = PhoenixKitEntities.create_entity(%{
  name: "product",
  display_name: "Product",
  display_name_plural: "Products",
  icon: "hero-cube",
  created_by_uuid: admin_user.uuid,
  fields_definition: [
    %{"type" => "text", "key" => "name", "label" => "Name", "required" => true},
    %{"type" => "number", "key" => "price", "label" => "Price"},
    %{"type" => "textarea", "key" => "description", "label" => "Description"},
    %{"type" => "select", "key" => "category", "label" => "Category",
      "options" => ["Electronics", "Clothing", "Food"]}
  ]
})

Create data records:

{:ok, record} = PhoenixKitEntities.EntityData.create(%{
  entity_uuid: entity.uuid,
  title: "iPhone 15",
  status: "published",
  created_by_uuid: admin_user.uuid,
  data: %{
    "name" => "iPhone 15",
    "price" => 999,
    "description" => "Latest iPhone model",
    "category" => "Electronics"
  }
})

Dependency types

Local development (path:)

{:phoenix_kit_entities, path: "../phoenix_kit_entities"}

Changes to the module's source are picked up automatically on recompile.

Git dependency (git:)

{:phoenix_kit_entities, git: "https://github.com/BeamLabEU/phoenix_kit_entities.git"}

After updating the remote: mix deps.update phoenix_kit_entities, then mix deps.compile phoenix_kit_entities --force + restart the server.

Hex package

{:phoenix_kit_entities, "~> 0.1.0"}

Project structure

lib/
  phoenix_kit_entities.ex              # Main module (schema + PhoenixKit.Module behaviour)
  phoenix_kit_entities/
    entity_data.ex                     # Data record schema and CRUD
    field_type.ex                      # Field type struct
    field_types.ex                     # Field type registry (12 types)
    form_builder.ex                    # Dynamic form generation + validation
    events.ex                          # PubSub broadcast/subscribe
    presence.ex                        # Phoenix.Presence for editing
    presence_helpers.ex                # FIFO locking, session tracking
    routes.ex                          # Admin + public route definitions
    sitemap_source.ex                  # Sitemap integration
    components/
      entity_form.ex                   # Embeddable public form component
    controllers/
      entity_form_controller.ex        # Public form submission handler
    migrations/
      v1.ex                            # Migration module (called by parent app)
    mirror/
      exporter.ex                      # Entity/data export to JSON
      importer.ex                      # Entity/data import from JSON
      storage.ex                       # File storage for mirror
    mix_tasks/
      export.ex                        # mix phoenix_kit_entities.export
      import.ex                        # mix phoenix_kit_entities.import
    web/
      entities.ex                      # Entity list LiveView (inline template)
      entity_form.ex                   # Entity definition builder LiveView
      data_navigator.ex                # Data record browser LiveView
      data_form.ex                     # Data record form LiveView
      data_view.ex                     # Data record read-only view
      entities_settings.ex             # Module settings LiveView
      hooks.ex                         # Shared LiveView hooks

Entity definitions

Entity definitions are blueprints for custom content types. Each entity has a name, display names, and a JSONB array of field definitions.

# List all entities
PhoenixKitEntities.list_entities()

# Get by name
PhoenixKitEntities.get_entity_by_name("product")

# Create
{:ok, entity} = PhoenixKitEntities.create_entity(%{...})

# Update
{:ok, entity} = PhoenixKitEntities.update_entity(entity, %{status: "published"})

# Delete (cascades to all data records)
{:ok, entity} = PhoenixKitEntities.delete_entity(entity)

Name constraints

Status workflow

Entities support three statuses: draft, published, archived.

Entity data records

Data records are instances of an entity definition. Field values are stored in a JSONB data column.

alias PhoenixKitEntities.EntityData

# List records for an entity
EntityData.list_by_entity(entity.uuid)

# Filter by status
EntityData.list_by_entity_and_status(entity.uuid, "published")

# Search by title
EntityData.search_by_title("iPhone", entity.uuid)

# Get by slug
EntityData.get_by_slug(entity.uuid, "iphone-15")

# CRUD
{:ok, record} = EntityData.create(%{...})
{:ok, record} = EntityData.update(record, %{...})
{:ok, record} = EntityData.delete(record)

Manual ordering

Entities can use auto sort (by creation date) or manual sort (by position). Configure via the entity's settings:

PhoenixKitEntities.update_sort_mode(entity, "manual")

Field types

Category Types Notes
Basic text, textarea, email, url, rich_text Rich text is HTML-sanitized
Numeric number Accepts integers and floats
Boolean boolean Toggle/checkbox
Date date Date picker
Choice select, radio, checkbox Require options array
Media file, image Coming soon
Relations relation Coming soon

Each field definition is a map with:

%{
  "type" => "text",          # Required
  "key" => "title",          # Required, unique per entity
  "label" => "Title",        # Required
  "required" => true,        # Optional, default false
  "default" => "",           # Optional
  "options" => ["A", "B"],   # Required for select/radio/checkbox
  "validation" => %{...}     # Optional validation rules
}

Use the helper functions:

alias PhoenixKitEntities.FieldTypes

FieldTypes.text_field("name", "Full Name", required: true)
FieldTypes.select_field("category", "Category", ["Tech", "Business"])
FieldTypes.boolean_field("featured", "Featured", default: true)

Admin UI

Admin routes are registered via PhoenixKitEntities.Routes (returned by route_module/0):

Route LiveView Purpose
/admin/entitiesWeb.Entities List all entity definitions
/admin/entities/newWeb.EntityForm Create entity definition
/admin/entities/:id/editWeb.EntityForm Edit entity definition
/admin/entities/:name/dataWeb.DataNavigator Browse entity records
/admin/entities/:name/data/newWeb.DataForm Create record
/admin/entities/:name/data/:uuidWeb.DataForm Edit record
/admin/settings/entitiesWeb.EntitiesSettings Module settings

Multi-language support

Multilang is auto-enabled when PhoenixKit has 2+ languages configured. Data is stored in a nested JSONB structure:

# Multilang data format
%{
  "en" => %{"title" => "Hello", "description" => "..."},
  "es" => %{"title" => "Hola", "description" => "..."}
}

See PhoenixKit.Utils.Multilang for helper functions.

Public forms

Entities can expose public submission forms. Enable in entity settings, then embed:

<EntityForm entity_slug="contact" />

Or use the controller endpoint. Public forms include:

Filesystem mirroring

Export and import entity definitions and data as JSON files:

mix phoenix_kit_entities.export
mix phoenix_kit_entities.import

Or programmatically:

PhoenixKitEntities.Mirror.Exporter.export_all(path)
PhoenixKitEntities.Mirror.Importer.import_all(path)

Events & PubSub

Subscribe to real-time events:

alias PhoenixKitEntities.Events

# Entity lifecycle
Events.subscribe_to_entities()
# Receives: {:entity_created, uuid}, {:entity_updated, uuid}, {:entity_deleted, uuid}

# Data lifecycle (all entities)
Events.subscribe_to_all_data()
# Receives: {:data_created, entity_uuid, data_uuid}, etc.

# Data lifecycle (specific entity)
Events.subscribe_to_entity_data(entity_uuid)

Available callbacks

This module implements PhoenixKit.Module with these callbacks:

Callback Value
module_key/0"entities"
module_name/0"Entities"
enabled?/0 Reads entities_enabled setting
enable_system/0 Sets entities_enabled to true
disable_system/0 Sets entities_enabled to false
permission_metadata/0 Icon: hero-cube-transparent
admin_tabs/0 Entities tab with dynamic entity children
settings_tabs/0 Settings tab under admin settings
children/0[PhoenixKitEntities.Presence]
css_sources/0[:phoenix_kit_entities]
route_module/0PhoenixKitEntities.Routes
get_config/0 Returns enabled status, limits, stats

Mix tasks

# Export all entities and data to JSON
mix phoenix_kit_entities.export

# Import entities and data from JSON
mix phoenix_kit_entities.import

Database

Database tables and migrations are managed by the parent PhoenixKit project. This repo provides PhoenixKitEntities.Migrations.V1 as a library module that the parent app's migrations call — there are no migrations to run in this repo directly.

# Two tables:
# phoenix_kit_entities       — entity definitions (blueprints)
# phoenix_kit_entity_data    — data records (instances)
# Both use UUIDv7 primary keys

Testing

# Create test database
createdb phoenix_kit_entities_test

# Run all tests
mix test

# Run only unit tests (no DB needed)
mix test --exclude integration

Troubleshooting

Module not appearing in admin

  1. Verify the dependency is in mix.exs and mix deps.get was run
  2. Check PhoenixKitEntities.enabled?() returns true
  3. Run PhoenixKitEntities.enable_system() if needed

"entities_enabled" setting not found

The settings are seeded by the migration. If using PhoenixKit core migrations, they're created by V17. If standalone, run the PhoenixKitEntities.Migrations.V1 migration.

Entity name validation fails

Names must be snake_case, start with a letter, 2-50 characters. Examples: product, team_member, faq_item. Invalid: Product, 123abc, a.

Changes not taking effect after editing

Force a clean rebuild: mix deps.clean phoenix_kit_entities && mix deps.get && mix deps.compile phoenix_kit_entities --force && mix compile --force

Note: This repo has no database migrations. All tables and migrations are managed by the parent PhoenixKit project. The test helper creates necessary DB functions directly when a test database is available.