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
- Dependency types
- Project structure
- Entity definitions
- Entity data records
- Field types
- Admin UI
- Multi-language support
- Public forms
- Filesystem mirroring
- Events & PubSub
- Available callbacks
- Mix tasks
- Testing
- Troubleshooting
What this provides
- Dynamic entity definitions with JSONB field schemas (no migrations per entity)
- 12 field types: text, textarea, email, url, rich_text, number, boolean, date, select, radio, checkbox, file
- Complete admin UI (LiveView) for managing entity definitions and data records
- Multi-language support (auto-enabled when 2+ languages are active)
- Collaborative editing with FIFO locking and presence tracking
- Public form builder with honeypot, time-based validation, and rate limiting
- Filesystem mirroring for export/import of entity definitions and data
- PubSub events for real-time updates across admin sessions
- Sitemap integration for published entity data
- Zero-config auto-discovery — just add the dependency
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:
- Admin sidebar (under Modules section) — browse entities and their data
- Admin > Modules — toggle it on/off
- Admin > Roles — grant/revoke access per role
- Admin > Settings > Entities — configure module settings
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 hooksEntity 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
- Must be unique, snake_case, 2-50 characters
-
Format:
^[a-z][a-z0-9_]*$ -
Examples:
product,team_member,faq_item
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/entities | Web.Entities | List all entity definitions |
/admin/entities/new | Web.EntityForm | Create entity definition |
/admin/entities/:id/edit | Web.EntityForm | Edit entity definition |
/admin/entities/:name/data | Web.DataNavigator | Browse entity records |
/admin/entities/:name/data/new | Web.DataForm | Create record |
/admin/entities/:name/data/:uuid | Web.DataForm | Edit record |
/admin/settings/entities | Web.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:
- Honeypot field for bot detection
- Time-based validation (minimum 3 seconds)
- Rate limiting (5 submissions per 60 seconds)
- Browser/OS/device metadata capture
Filesystem mirroring
Export and import entity definitions and data as JSON files:
mix phoenix_kit_entities.export
mix phoenix_kit_entities.importOr 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/0 | PhoenixKitEntities.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.importDatabase
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 keysTesting
# Create test database
createdb phoenix_kit_entities_test
# Run all tests
mix test
# Run only unit tests (no DB needed)
mix test --exclude integrationTroubleshooting
Module not appearing in admin
-
Verify the dependency is in
mix.exsandmix deps.getwas run -
Check
PhoenixKitEntities.enabled?()returnstrue -
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.