Why this plugin is the canonical reference
Most plugins in production are small. A sending driver is one class plus a connection blade. A payment gateway is a service plus a redirect controller. A simple sidebar add-on is one Hook::add call. None of those exercise the whole plugin SDK — and small plugins are not the right reading exercise for an author trying to understand how big a plugin can responsibly grow.
acelle/ai exists at the other end of the spectrum. It is a self-contained AI subsystem: an agent chatbox, universal text-rewrite components, KB-grounded coach personas, and an admin observability dashboard. Activating the plugin in an Acelle install adds the entire AI surface without touching core code; deactivating removes it cleanly. The plugin uses every concept the rest of these docs cover, in production. Reading it is the fastest way to see how the patterns combine when the feature is non-trivial.
The file tree at a glance
From the plugin's own README:
| Folder | What it owns |
src/AIHandler/ | AI runtime engine — engines, agent loop, tools, settings resolver, observability writer/reader, KB lookup, URL sanitiser. |
src/Models/ | Eight Eloquent models for the audit substrate (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord). |
src/Controllers/ | Admin controllers (/rui/admin/ai-*) + public-API controllers (/api/v1/ai/*) + the plugin-landing PluginDashboardController. |
src/Services/ | PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy, etc. |
src/ServiceProvider.php | Sole entry point — registers PSR-4, routes, views, lang files, hooks, middleware aliases, lifecycle listeners. |
database/migrations/ | Fourteen audit-substrate migrations. Run on activate, rolled back on delete. |
resources/views/ | Sixty-plus admin Blade templates + three universal anonymous components (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + chatbox / sparkle JS partials. |
resources/assets/ | CSS (~14 files) + JS (~21 files) published to public/plugins/acelle/ai/ via vendor:publish --tag=plugin --force on plugin install. |
resources/lang/ | Eighteen locales × nine language files = the AI module's full surface translated. |
tests/ | One hundred-plus Pest tests (Feature + Unit) + the Acelle\Ai\Tests\PluginTestCase base class. |
routes.php | Plugin routes (admin + public API + the plugin-landing dashboard at /plugins/acelle/ai/dashboard). |
composer.json | Plugin metadata; extra.setting-route points at PluginDashboardController@index so the admin Plugins page "Settings" button deep-links into the plugin's own dashboard. |
None of those folders are bespoke. Every one maps directly to a section in the rest of these docs. Reading the plugin is a process of recognising the same pattern shipped at scale.
Eight Eloquent models — the audit substrate
The AI module's data layer is shaped around auditability: every conversation, every model invocation, every tool call, every user feedback, and every blob of raw provider output is captured for replay and observability. Eight models cover that substrate:
| Model | Table | What it represents |
AIConversation | ai_conversations | One row per multi-turn agent / support session. Carries customer + user FKs, task key, screen route, and rolled-up token / cost totals. |
AIMessage | ai_messages | One row per user / agent turn. Role, content JSON, FK to a tool-call, latency, model used. |
AIRequest | ai_requests | One row per upstream API call. Engine, prompt hash, latency, cost, error status. Bridges AIMessage to the actual HTTP traffic. |
AIToolCall | ai_tool_calls | Function-call invocations spawned by an agent turn. Tool name, input/output JSON, source flag. |
AIFeedback | ai_feedback | Thumbs-up/down + free-text feedback per message + per conversation. |
AIRawBlob | ai_raw_blobs | Original raw provider responses, kept for replay / audit. Separate table because the rollup table needs to stay small. |
AIDailyRollup | ai_daily_rollup | Per-day aggregate for the admin observability dashboard — token totals, cost, error rate. Pre-aggregated so the dashboard reads cheaply. |
AIToolUndoRecord | ai_tool_undo_records | Tracks reversible tool actions for the "undo last" feature. |
Three patterns from this list translate directly into other plugins. Splitting "raw provider responses" into a separate table from "rolled-up summary" lets the rollup table stay small enough to scan. Nullable FKs to customers and users let the same row work for authenticated and anonymous traffic. One-row-per-day rollups give the admin dashboard cheap reads without a heavy JOIN against the activity tables.
Fourteen migrations one-line each
The migration filenames in storage/app/plugins/acelle/ai/database/migrations/ tell their own story — additive over time, immediately reversible, never a destructive schema move:
| Filename | What it does |
2026_04_28_000001_create_ai_conversations_table.php | Multi-turn chat sessions — uid, customer_id FK, status enum, token / cost rollups |
2026_04_28_000002_create_ai_messages_table.php | Single user / agent turn — role, content JSON, tool-call FK, latency, model used |
2026_04_28_000003_create_ai_requests_table.php | One row per upstream API call — engine, prompt hash, latency, cost, error |
2026_04_28_000004_create_ai_tool_calls_table.php | Function-call invocations spawned by an agent turn — input / output JSON |
2026_04_28_000005_create_ai_feedback_table.php | Thumbs-up/down + free-text feedback per message + per conversation |
2026_04_28_000006_create_ai_raw_blobs_table.php | Original raw provider responses, kept for replay / audit |
2026_04_28_000007_create_ai_daily_rollup_table.php | Per-day aggregate for the admin dashboard — token totals, cost, error rate |
2026_04_29_000001_add_client_message_id_to_ai_messages.php | Cross-tab dedup column — additive, no defaults |
2026_04_30_000002_add_source_to_ai_tool_calls.php | Tracks whether a tool call came from agent vs support route |
2026_05_02_180000_widen_ai_conversations_client_session_uid.php | ULID / UUID width fix — column-altering migration, fully reversible |
2026_05_02_200000_create_ai_tool_undo_records_table.php | Tracks reversible tool actions for the "undo last" feature |
2026_05_03_000001_add_url_sanitization_to_ai_requests.php | Adds a JSON column for sanitised URL telemetry |
2026_05_04_000001_create_ai_settings_table.php | Plugin-level admin settings — kept separate from plugins.data so each row can be indexed |
Read the migrations top-to-bottom and you have the schema evolution of the entire AI module. Every additive migration is a feature ship — the additive shape is what makes the plugin's delete_plugin_* rollback always work, even when an admin uninstalls after a year of feature accretion.
Every hook the plugin uses
The plugin's ServiceProvider exercises every hook pattern except FILTER. A grep for Hook::* against storage/app/plugins/acelle/ai/src/ServiceProvider.php:
REGISTRY (Hook::add) — six contributions
- Nine
add_translation_file entries at register() lines 175-197 — one per translation surface (rewrite, chatbox, prompts, wait, subject AB, settings, admin usage, audit, permissions). Each surface is a separately editable file in the admin Languages UI.
layout.head.assets at boot() line 688 — contributes chatbox CSS + JS to every page that extends the master app / admin layouts.
layout.body.before_close at boot() line 699 — contributes the chatbox bubble HTML and the sparkle popover before every page's </body>.
admin.sidebar.groups at boot() line 718 — adds the AI group with its three or four child links to the admin sidebar.
All six gate themselves with aiPluginAvailable() — a helper that ultimately resolves to Plugin::getByName('acelle/ai')->isActive(). Returning null when the plugin is gated off is the conventional way to disappear cleanly without unloading the service provider.
EVENT (Hook::on) — two lifecycle listeners
activate_plugin_acelle/ai at line 472 — runs artisan migrate against the plugin's migrations folder.
delete_plugin_acelle/ai at line 546 — accepts the $keepData flag, rolls back migrations when not set, preserves the audit tables when set.
BEHAVIOR (Hook::set) — one icon URL override
icon_url_acelle/ai at line 522 — overrides the per-plugin BEHAVIOR hook so the admin Plugins page renders the AI module's own icon instead of the host's default plugin.svg.
Together these are most of the plugin's hook surface. Reading ServiceProvider.php top-to-bottom is the fastest way to see how the patterns combine in production.
The chatbox UI surface
The plugin contributes three universal Blade components in resources/views/components/ — none of them require the host application to know they exist:
<x-mc-ai-chatbox> — the floating chatbox bubble that opens into a multi-turn agent conversation. Mounted via the layout.body.before_close REGISTRY hook so it appears on every app + admin page.
<x-mc-ai-rewrite> — universal "rewrite this text" affordance that can be dropped next to any textarea in the host. Plugin-namespaced, no central registration.
<x-mc-ai-subject-ab-generator> — generates A/B subject-line variants from a prompt. Used in the campaign editor.
These three components show the pattern for "plugin contributes UI to multiple host pages without forking each page": ship the component as an anonymous Blade component under your plugin's view namespace, register it through the layout REGISTRY hooks for global mounting, or have host pages opt-in by including it directly. Both patterns work; the AI plugin uses both.
Nine files × eighteen locales
The plugin's register() registers nine separate translation files. The Language::dump() machinery then materialises each into seventeen non-English locale runtime folders under storage/app/data/plugins/acelle/ai/lang/. The result on disk: 153 runtime translation files (9 files × 17 non-English locales + 9 English originals = 162 minus the 9 English masters = 153 dump-clones), each separately editable through the host's Languages admin UI.
The nine surface files (registered via the $aiLangFiles loop in ServiceProvider::register()):
ai_rewrite — the universal text rewrite component
ai_chatbox — the chatbox UI
ai_chatbox_prompts — pre-canned prompts shown in the chatbox
ai_chatbox_wait — the smart-wait UI ("looking up… running tool… composing reply")
ai_subject_ab — the subject A/B generator
ai_settings — admin settings page labels
admin_ai_usage — admin usage / cost dashboard
admin_ai_audit — admin audit / replay UI
admin_ai_permissions — admin per-feature permission toggles
This split is the practical answer to "how do I keep translation files small enough that a translator can edit one in a single sitting": one master per logical surface, registered separately, materialised per locale, edited independently through the admin UI.
Plugin-owned config files
The plugin owns two config files under config/ — registered in ServiceProvider::boot() via $this->mergeConfigFrom() and reachable through the standard config('ai.*') and config('ai-navigation-hints.*') helpers. Plugin-owned config is the right place for static metadata that does not change per-install (engine catalog, prompt templates, navigation defaults); admin-editable settings live in the ai_settings table seeded by migration.
The split — config for plugin-shipped immutable defaults, DB rows for admin-editable mutable settings — is a pattern that ports cleanly to other plugins. Putting both in the plugins.data JSON column is tempting but punishes admin UI performance; a dedicated indexed table was the right call.
The test infrastructure
The plugin's test directory follows the host's tests/ shape — Unit + Feature folders plus a PluginTestCase base class at the root:
storage/app/plugins/acelle/ai/tests/
├── PluginTestCase.php ← seeds the plugin row as active before every test
├── Feature/
│ ├── AIHandler/ ← engines, agent loop, tools, observability writer
│ └── PluginLifecycle/ ← lifecycle integration tests
├── Unit/ ← isolated unit tests (no Laravel boot for some)
├── Fixtures/ ← test fixtures + factories
├── Snapshots/ ← Pest snapshot artefacts
└── Support/ ← test-only helpers
The host's phpunit.xml registers the plugin's testsuite as <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" runs the full suite in parallel with the host's own Unit + Feature suites.
The PluginTestCase at the root demonstrates the per-request gate-cache reset trap that every plugin shipping middleware should adopt — see the Testing § gate-cache trap for the full pattern. Without it, the second test in the suite onward observes stale cache state from the first test's boot.
How to learn from it
Reading every line of a hundred-thousand-token plugin is not the right exercise. A four-step recipe is more useful:
-
Clone or symlink the plugin into a host install. Activate it. Open the admin sidebar — the AI group should appear. Click "Settings" on the admin Plugins page entry — the plugin-landing dashboard should load. This proves the plugin's UI surface is wired into your local host.
-
Read
src/ServiceProvider.php top-to-bottom. Forty minutes. Every concept covered in Plugin architecture + Hook system + UI injection + Translations appears in this one file at production scale. Cross-reference with the deep-dive pages as you go.
-
Trace one feature end-to-end. Pick the chatbox bubble. Find the entry point (
layout.body.before_close hook in ServiceProvider), follow the partial it returns (ai::partials.body_assets), find the anonymous component that renders the bubble, find the JS that mounts it, find the API route the JS calls, find the controller, follow it down to the AIHandler runtime engine, watch the AIConversation + AIMessage + AIRequest rows get inserted. Two-to-three hours, end-to-end. After this you will know which patterns the AI plugin uses and which it does without.
-
Adapt one pattern to your own plugin. Pick the smallest one that maps — usually the per-translation-surface registration loop, or the admin sidebar group, or the per-day rollup-table approach. Strip out the AI-specific code; keep the structural pattern. That is the plugin author's day-one productivity gain.
Where to go next
This is the last of the eleven developer deep-dives. From here, the index hub is the natural recap: Documentation index shows every page in the cluster organised into Foundation / Building / Quality / Reference. The developer landing is the marketing-shaped entry point for new visitors arriving from search.
Two practical next steps when you are ready to ship a real plugin: the activate → test → delete cycle from the Testing deep-dive proves the plugin's delete_plugin_* hook listener cleans up correctly. Plugin architecture § Recovery from broken state is the page to bookmark for production runtime issues — three failure modes plus the exact fix path each.
Beyond AcelleMail-specific patterns, the broader Laravel ecosystem applies. Plugin code is vanilla Laravel; the host runtime loader is the only non-standard piece. Anything you know about Eloquent, Blade, Pest, queues, scheduling, or middleware works inside the plugin folder unchanged.