Self-hosted email marketing with full source code. Pay once, own forever. Get AcelleMail — $74 →

FOR DEVELOPERS

Build plugins. Not workarounds.

AcelleMail is a Laravel 11+ email marketing platform that ships with full unencrypted PHP source — and a typed Hook system that lets your plugins add sending drivers, payment gateways, AI, custom UI, even REST APIs. Without forking core.

Real plugins shipping today

acelle/ai athena/evs rencontru/postal acelle/console
myvendor/loyalty/ PLUGIN SOURCE composer.json 240 B routes.php 1.2 KB src/ ServiceProvider.php entry Controllers/ Models/ database/migrations/ resources/ views/ lang/ index.json auto $ php artisan plugin:init … HOOK SYSTEM no core touches ACELLEMAIL Laravel 11+ · PHP 8.3+ layout.head.assets layout.body.close admin.sidebar.groups register_sending_* page.{ctrl}.{slot} customer_added sidebar-menu-items dispatch_*_job + many more…

WHY DEVELOPERS CHOOSE ACELLEMAIL

Built on the framework you already know.
Extended through patterns you already use.

Open source PHP / Laravel

Full unencrypted PHP source ships with every license. Modify any class, override any service, fork into your own namespace — your code, your rules. Built on Laravel 11+ so the framework you already know does the heavy lifting.

$ grep -r "protected function" vendor/acelle/

Plugin architecture

Drop a Composer-shaped folder under storage/app/plugins/<vendor>/<name>/ and the app autoloads it. ServiceProvider injects routes, views, migrations, hooks — activate, deactivate, delete cleanly.

$ php artisan plugin:init myvendor/loyalty

Token-auth REST API

CRUD endpoints for campaigns, lists, subscribers, templates, automations. Webhook events for every domain-level happening. Drop-in for SaaS frontends, mobile apps, or other Laravel services. Same API plugins use is the same API external clients use — no second-class endpoints.

GET /api/v1/campaigns · POST /api/v1/lists

Self-hosted on your infra

Your server, your DB, your subscriber data. No vendor lock-in. Pay once for the license; pay only the underlying SMTP cost (e.g. Amazon SES at $0.10/1K). Deploy anywhere PHP runs — bare metal, Kubernetes, Forge, Laravel Vapor. Scale however your stack scales.

· PHP 8.3+ · MySQL / MariaDB · Redis (optional)

WHAT YOU CAN BUILD WITH PLUGINS

14 extension surfaces.
Every one with a real production example today.

A plugin is a self-contained Laravel package that lives on disk, has its own namespace, its own database tables, routes, views, controllers, models — and integrates with core through a typed Hook system instead of include hacks.

Drivers & integrations · 4

Sending servers / drivers REGISTRY
register_sending_server_driver
rencontru/postal — Postal MTA driver as a single drop-in plugin
Email verification / deliverability REGISTRY
REGISTRY + FILTER
athena/evs — verification subsystem with admin UI
Payment gateways REGISTRY
REGISTRY
Built-in Stripe / PayPal / Braintree / Paystack / Razorpay / Coinbase show the pattern
AI / chatbot / assistants REGISTRY
REGISTRY + EVENT
acelle/ai — chatbox + sparkle + observability

UI & pages · 5

Custom UI injection REGISTRY
layout.head.assets / layout.body.before_close / admin.sidebar.groups
acelle/ai chatbox + sparkle popover
Page-level content slots REGISTRY
page.{ctrl}.{action}.{slot}
page.maillist.show.body, page.campaign.index.sidebar
Admin pages ROUTE
Laravel routes
/rui/admin/ai-usage, /rui/admin/ai-audit, /rui/admin/ai-conversations
Customer-facing pages ROUTE
Laravel routes
/plugins/acelle/ai/dashboard
REST APIs ROUTE
Routes + api.access_token
/api/v1/ai/* in acelle/ai

Data & lifecycle · 2

Custom Eloquent models + DB tables BEHAVIOR
Plugin-isolated migrations
acelle/ai ships 8 models / 12 migrations, vendor-prefixed table names
Translations (18 locales) REGISTRY
add_translation_file
acelle/ai ships 162 lang files (18 × 9)

Behavior & flow · 3

Webhook listeners / domain events EVENT
Hook::on(…)
customer_added, plan_changed, subscription_terminated
Behavior overrides BEHAVIOR
Hook::set / setIfEmpty / perform
dispatch_list_import_job — replace import logic entirely
Filter chains FILTER
Hook::modify / filter
sidebar-menu-items, content pre-send mutation, redirect URLs

A single plugin can mix any of these surfaces — acelle/ai uses 7 of 14 in one package.

THE HOOK SYSTEM

Four typed extension patterns. No surprise overrides. No silent conflicts.

Core never imports plugin code — it only declares extension points. Plugins listen and react. Four patterns, each with a clear role. Conflicts throw immediately.

1

REGISTRY add() + collect()

Plugin contributes items to a list. Core asks "who has a sending driver?" and your plugin says "I have one."


Hook::add('register_sending_server_driver', fn () => [
    'type'   => 'myvendor-foo',
    'driver' => \MyVendor\Foo\Driver::class,
]);


$drivers = Hook::collect('register_sending_server_driver');
2

EVENT on() + fire()

Plugin reacts to something happening. Return values discarded.


Hook::on('customer_added', function ($customer) {
    LoyaltyPoints::award($customer, 100, 'welcome');
});


Hook::fire('customer_added', [$customer]);
3

BEHAVIOR set() + perform()

Plugin replaces a piece of core logic completely. Only one plugin can claim a given behavior — conflicts throw immediately.


Hook::set('dispatch_list_import_job',
    fn ($list, $f) => new MyFasterImportJob($list, $f));


Hook::setIfEmpty('dispatch_list_import_job',
    fn ($list, $f) => new DefaultImportJob($list, $f));
$job = Hook::perform('dispatch_list_import_job', [$list, $f]);
dispatch($job);
4

FILTER modify() + filter()

Plugin transforms a value through a chain of callbacks. Each callback receives the current value, returns the modified value.


Hook::modify('sidebar-menu-items', function (array $items) {
    $items[] = [
        'label' => 'Loyalty',
        'url'   => route('lp.dashboard'),
    ];
    return $items;
});


$menu = Hook::filter('sidebar-menu-items', $defaultMenu);

HELLO WORLD IN 5 MINUTES

A working plugin from zero in eight commands.

The scaffold creates everything — Composer metadata, ServiceProvider, sample model, sample migration, sample route, sample view. From there it's yours to grow.

  1. 1

    Scaffold the plugin

    Creates the standard folder layout + an inactive DB row.

    bash
    $ php artisan plugin:init myvendor/loyalty
      ✓ Created storage/app/plugins/myvendor/loyalty/
      ✓ DB row inserted  (status: inactive)
      ✓ index.json + ServiceProvider autoloaded
  2. 2

    Edit composer.json

    Tell Composer about your namespace and the ServiceProvider.

    json
    {
      "name": "myvendor/loyalty",
      "description": "Award points on signup",
      "version": "1.0.0",
      "autoload":  { "psr-4": { "MyVendor\\Loyalty\\": "src/" } },
      "extra":     { "laravel": { "providers": [
        "MyVendor\\Loyalty\\ServiceProvider"
      ] } }
    }
  3. 3

    Write a migration

    Plugin-isolated table, vendor-prefixed name. Runs on activate, rolls back on delete.

    php
    Schema::create('myvendor_loyalty_accounts', function (Blueprint $t) {
        $t->id();
        $t->foreignId('customer_id')->constrained()->cascadeOnDelete();
        $t->integer('points')->default(0);
        $t->timestamps();
    });
  4. 4

    Define a model

    Standard Eloquent model in your own namespace.

    php
    namespace MyVendor\Loyalty\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class Account extends Model
    {
        protected $table    = 'myvendor_loyalty_accounts';
        protected $fillable = ['customer_id', 'points'];
    }
  5. 5

    Add a controller and view

    Plain Laravel controller — view namespace is your plugin name.

    php
    namespace MyVendor\Loyalty\Controllers;
    
    use Acelle\Http\Controllers\Controller;
    
    class DashboardController extends Controller
    {
        public function index()
        {
            return view('loyalty::dashboard', [
                'accounts' => \MyVendor\Loyalty\Models\Account::orderByDesc('points')->take(10)->get(),
            ]);
        }
    }
  6. 6

    Declare routes

    Same Route::group you already use. Loaded by ServiceProvider::boot().

    php
    Route::group([
        'middleware' => ['web', 'auth'],
        'namespace'  => '\MyVendor\Loyalty\Controllers',
        'prefix'     => 'plugins/myvendor/loyalty',
    ], function () {
        Route::get('/',          'DashboardController@index')->name('lp.dashboard');
        Route::get('/{account}', 'DashboardController@show');
    });
  7. 7

    Wire it all up in ServiceProvider::boot()

    Load views + routes; hook into activate_plugin_… to migrate on activate.

    php
    public function boot(): void
    {
        $this->loadViewsFrom (__DIR__.'/../resources/views', 'loyalty');
        $this->loadRoutesFrom(__DIR__.'/../routes.php');
    
        Hook::on('activate_plugin_myvendor/loyalty', fn () =>
            \Artisan::call('migrate', [
                '--path'  => 'storage/app/plugins/myvendor/loyalty/database/migrations',
                '--force' => true,
            ])
        );
    
        Hook::on('customer_added', fn ($customer) =>
            Models\Account::create(['customer_id' => $customer->id, 'points' => 100])
        );
    }
  8. 8

    Activate from admin

    Go to /rui/admin/plugins. Migrations run automatically. Plugin lives at its prefix.

    bash
    ✓ activated  myvendor/loyalty  v1.0.0
      → migrations: 1 ran (myvendor_loyalty_accounts)
      → routes:     2 registered under /plugins/myvendor/loyalty
      → hooks:      2 listening (activate, customer_added)
      → status:     active
    
    → visit /plugins/myvendor/loyalty/  ← live

You now have a working plugin. Add as many models, controllers, hooks as your feature demands. Tests live in tests/; run them with php artisan test --testsuite='Plugin: myvendor/loyalty'. Source the canonical contract at knowledge.acellemail.com.

PLUGIN LIFECYCLE

Four states. Predictable transitions. No "did it run?" mystery.

Every plugin lives in exactly one of four states. The state you're in dictates which hooks fire, which migrations have run, and what happens on the next transition. Source-grounded against docs/plugin/SOURCE_OF_TRUTH.md.

STATE 01 register (inactive) Plugin folder discovered. index.json + DB row exist. Hooks NOT firing. STATE 02 active All hooks live. Routes mounted. Migrations have run. activate_plugin_… fired once. STATE 03 inactive (paused) Folder still on disk. Hooks paused. Routes unmounted. DB tables intact. STATE 04 deleted (gone) Folder removed. Migrations rolled back if $keepData = false. DB row removed. activate migrations run deactivate data kept remove $keepData? re-activate activate hook does NOT re-run
REGISTER · INACTIVE

Discovered, not running

Plugin folder exists, index.json indexed, DB row says inactive. ServiceProvider's register() ran, but boot()'s hooks are gated off until you click Activate.

ACTIVE

Live, hooks firing

First activation fires activate_plugin_<name> once — this is where migrations run. After that all REGISTRY / EVENT / FILTER / BEHAVIOR hooks are live until deactivated.

INACTIVE · PAUSED

Paused, data intact

Routes unmount, hooks gate off, but the folder + DB tables stay. Re-activate doesn't re-run migrations or the activate hook — safe to toggle in production.

DELETED

Removed cleanly

Folder removed, DB row gone. If $keepData = false, plugin migrations roll back — vendor-prefixed tables drop. Core tables never touched.

REAL PLUGIN SHOWCASE

acelle/ai — a full AI subsystem in a single plugin folder.

The acelle/ai plugin ships a complete AI assistant layer: agent chatbox on every page, ✨ sparkle text rewrite on rich-text fields, KB-grounded coach personas (Deliverability, Segments, Campaigns, Forms), and an admin observability surface across 5 routes. It mounts via 3 layout hooks. Zero core changes.

acelle/ai plugin in action — floating AcelleMail AI chatbox bubble with the ✨ sparkle popover for one-click rewrite, plus the plugin's admin sidebar group rendered inside the AcelleMail UI

What it ships

  • Floating chatbox — plain support mode + opt-in agent mode that calls tools
  • Sparkle popover — one-click rewrite / expand / simplify on every rich-text field
  • Coach personas — domain-aware LLM helpers, scoped per screen
  • Admin observability — usage, audit, conversations, feedback, self-improve
  • Engine flexibility — BYO OpenAI, Anthropic, or local Ollama
  • Plugin landing page at /plugins/acelle/ai/dashboard

By the numbers

8
Eloquent models
12
Migrations
60+
Blade templates
100+
Pest tests
18 × 9
Locales × lang files
~35
CSS + JS files

How it mounts — 3 hooks

acelle/ai plugin → CSS / JS assets Hook::add( 'layout.head.assets', fn()=>...); → Chatbox bubble Hook::add( 'layout.body.before_close' ,fn()=>...); → AI sidebar group Hook::add( 'admin.sidebar.groups', fn()=>...); Self-contained. AcelleMail core <head> + acelle-ai-chatbox.css </body> (just before) + <div class="ai-chatbox"> Admin sidebar AI · Settings AI · Usage AI · Conversations

3 layout REGISTRY hooks — that's it. Plus per-page injections, lifecycle event listeners, and a custom REST API namespaced at /api/v1/ai/*. Zero core changes; the plugin folder is drop-in deployable.

REST API

Token-authenticated endpoints. Same API plugins use.

Use the REST API to integrate AcelleMail into your own SaaS frontend, mobile app, or Laravel service. The same API plugins use is the same API external clients use — no second-class endpoints.

Resources

  • ListsGET/POST/PATCH/DELETE /api/v1/lists[/:uid]
  • Subscribers/api/v1/subscribers · tags · sub/unsub
  • Campaigns/api/v1/campaigns[/:uid] · run/pause/resume · log CSVs
  • Automations/api/v1/automations[/:uid] · execute · api/call
  • Sending servers/api/v1/sending_servers[/:uid]
  • Customers (admin)/api/v1/customers[/:uid] · assign/change-plan · enable/disable
  • Subscriptions (admin)/api/v1/subscriptions[/:uid]

Webhook events

Lifecycle (configured in Admin → Webhooks): new_customer, new_subscription, change_plan, cancel_subscription, terminate_subscription, automation_webhook.
Per-recipient tracking (per campaign): open, click, unsubscribe.

Full API reference →
# Create a list
$ curl -X POST \
    https://your-acelle.example.com/api/v1/lists \
    -H "Authorization: Bearer $ACELLE_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Newsletter",
      "from_email": "hi@example.com",
      "from_name": "Hi Team",
      "subject": "Weekly digest"
    }'

# Add a subscriber
$ curl -X POST \
    .../api/v1/lists/:uid/subscribers \
    -H "Authorization: Bearer $ACELLE_TOKEN" \
    -d '{"EMAIL":"alice@example.com"}'

# Send a campaign
$ curl -X POST \
    .../api/v1/campaigns/:uid/send \
    -H "Authorization: Bearer $ACELLE_TOKEN"

# Receive a webhook
POST /your-webhook-endpoint
{
  "event": "campaign.sent",
  "campaign_uid": "abc123",
  "list_uid": "xyz789",
  "sent_at": "2026-05-06T12:34:56Z"
}

OPEN-SOURCE ETHOS

Your code, forever. No black boxes, no recurring fees, no namespace squatting.

Unencrypted source

Every line of PHP that runs on your server. No ionCube, no SourceGuardian, no obfuscation. git diff your customizations. grep to find anything.

Lifetime updates

Every release of AcelleMail since 2016 is in your CodeCanyon download history. Pull what you want when you want it. No subscription pressure.

Your namespace, forever

Plugins live under MyVendor\MyPlugin\ — completely isolated from Acelle\* core. Composer-distributable to your team or marketplace.

One-time license

Pay $74 (Regular) or $199 (Extended for SaaS). No subscriptions, no per-subscriber, no per-email. The math doesn't change as you grow.

RESOURCES & COMMUNITY

Docs, sample plugins, and a direct line to support.

Full developer documentation

Eleven source-grounded deep-dives. Jump straight to the page you need:

Browse hub →

Plugin Development Guide

Five host-side files, the boot-and-load flow, the four lifecycle states, the two injection layers — the mental model the rest of the docs assume.

/developers/plugin-architecture →

REST API Reference

Authentication, endpoint catalog, webhook payloads, error shapes — native landing page with examples.

/api →

The Hook system — four patterns

REGISTRY / EVENT / BEHAVIOR / FILTER — the four extension shapes plugins use, with real call-sites grepped from core, conflict semantics, and six anti-patterns.

/developers/hook-system →

Sending drivers — full plugin pattern

Ship a brand-new MTA backend without forking core. The register_sending_server_driver contract, nine capability markers, and five Postal-plugin pitfalls.

/developers/sending-drivers →

Plugin showcase — acelle/ai

The canonical complex plugin walked end-to-end. Eight models, fourteen migrations, eighteen locales, every hook surface, the chatbox UI, plus a four-step learning recipe.

/developers/showcase →

Developer support

Stuck on a hook contract or plugin lifecycle question? Email us — typical response within 24h.

support@acellemail.com →
Documentation index · CAT

Browse all 11 deep-dives →

Foundation, Building, Quality, Reference — every page in one place, source-grounded against storage/app/plugins/acelle/ai/ and the canonical plugin docs.

/developers →

DEVELOPER FAQ

The questions developers actually ask before buying.

Can I modify the core source code without losing updates? +

Yes, but the cleanest pattern is a plugin. Plugins live outside the core namespace and survive every upgrade automatically. Direct core edits work but require manually merging upstream changes each release. The plugin system exists exactly to avoid that toil.

Do plugins survive AcelleMail upgrades? +

Yes, by design. Core declares hook points; plugins react. As long as the hook name and signature stay stable (and we version-stamp breaking changes), your plugin keeps working. We test the AI plugin (acelle/ai) on every release.

Can I sell plugins commercially? +

Yes. The Extended License lets you distribute your own plugins under your own license. Plugins live in their own namespace; you own that code 100%. Several teams run private marketplaces for their internal plugin ecosystems.

How does the plugin system handle conflicts? +

REGISTRY hooks merge (every plugin contributes); EVENT hooks all fire (no conflict possible); FILTER chains accumulate; BEHAVIOR is the only "exclusive" pattern — if two plugins try to claim the same behavior, an exception throws immediately. No silent override surprises.

Can plugins talk to each other? +

Yes, via the same Hook system. Plugin A can fire events for Plugin B to listen on. Or expose a public class — MyVendor\PluginA\Service is a regular PHP class, importable like any other. The acelle/ai plugin exposes hooks for other plugins to register custom AI tools.

What's the difference between Regular and Extended for a developer? +

Regular ($74) covers your own use on a single domain. Extended ($199) adds the right to charge end-users (resell as SaaS), distribute white-label clones, and sell your own plugins. Both ship the same source code.

Do plugin migrations touch the main DB? +

They write to the same database, but in tables prefixed by your vendor name (e.g. myvendor_loyalty_accounts). Migrations run on plugin activate, roll back on plugin delete with $keepData = false. Zero risk to core tables.

Can I write plugins in something other than PHP? +

The plugin runtime is PHP/Laravel. But your plugin can shell out to anything — Node services, Python ML, Go binaries, external HTTP APIs. Several teams ship plugins that wrap external tools. The plugin is the integration shell; the heavy lifting can live anywhere.

How do plugins interact with the AI subsystem? +

acelle/ai exposes hooks that other plugins can use to register custom AI tools, swap engines, or hook into the observability layer. Read the AI plugin source for the contract. Your plugin can also fire events that the AI agent picks up as context.

Is there a plugin marketplace? +

Not yet. Today, plugins distribute via direct sale, GitHub, or your private registry. A community marketplace is on the roadmap once enough third-party plugins exist to make one worthwhile.

Ship your first plugin this afternoon.

Full PHP source code. Lifetime updates. The Hook system. Real production examples to copy from. One-time $199 for the Extended License (commercial plugin redistribution + SaaS use).

Buy Extended License — $199 Try Live Demo