# 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](#what-this-provides)
- [Quick start](#quick-start)
- [Dependency types](#dependency-types)
- [Project structure](#project-structure)
- [Entity definitions](#entity-definitions)
- [Entity data records](#entity-data-records)
- [Field types](#field-types)
- [Admin UI](#admin-ui)
- [Multi-language support](#multi-language-support)
- [Public forms](#public-forms)
- [Filesystem mirroring](#filesystem-mirroring)
- [Events & PubSub](#events--pubsub)
- [Available callbacks](#available-callbacks)
- [Mix tasks](#mix-tasks)
- [Testing](#testing)
- [Troubleshooting](#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`:
```elixir
{: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:
```elixir
PhoenixKitEntities.enable_system()
```
Create your first entity:
```elixir
{: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:
```elixir
{: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:`)
```elixir
{:phoenix_kit_entities, path: "../phoenix_kit_entities"}
```
Changes to the module's source are picked up automatically on recompile.
### Git dependency (`git:`)
```elixir
{: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
```elixir
{: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.
```elixir
# 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.
```elixir
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`:
```elixir
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:
```elixir
%{
"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:
```elixir
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:
```elixir
# 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:
```html
<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:
```bash
mix phoenix_kit_entities.export
mix phoenix_kit_entities.import
```
Or programmatically:
```elixir
PhoenixKitEntities.Mirror.Exporter.export_all(path)
PhoenixKitEntities.Mirror.Importer.import_all(path)
```
## Events & PubSub
Subscribe to real-time events:
```elixir
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
```bash
# 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.
```elixir
# Two tables:
# phoenix_kit_entities — entity definitions (blueprints)
# phoenix_kit_entity_data — data records (instances)
# Both use UUIDv7 primary keys
```
## Testing
```bash
# 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.