Plugins

Adding features without editing the product: install a plugin from a .zip, or write your own with actions and filters.

A plugin runs with the same rights as the application. It can read and change anything, including your customers' data. Install plugins only from people you trust.

Installing one

Plugins: those installed, and the form to install one. None are installed here. Plugins: those installed, and the form to install one. None are installed here.

Settings → Plugins lists the Installed plugins: each one's name and version, an Active badge when it is switched on, its description, and its slug and author.

  1. Under Install a plugin, choose the Plugin .zip — a .zip with plugin.json at the top, up to 20 MB — and press Upload and install. It is installed but not switched on.
  2. Press Activate on it and confirm. The confirmation says it will run with the same rights as the application. Any database tables the plugin brings are created now.

Uploading a newer version of an installed plugin replaces its files and keeps it active, running any new database changes it brings. Or copy the plugin's folder into plugins/ on the server and activate it from the same screen.

Switching one off, and deleting it

Installing, activating, deactivating and deleting are each written to the application log, with who did it.

The screen is for administrators, and in SaaS mode for the super admin only — a plugin would run for every workspace on the install. Uploading can be switched off entirely with PLUGIN_UPLOADS=false in .env, leaving only plugins copied onto the server; the screen then says so. It also says when the plugins/ folder cannot be written by the web server, in which case nothing can be installed from here.

Every archive is checked before anything is written: a file that would land outside the plugin's own folder, a symbolic link, or more than 100 MB unpacked, and it is refused whole.

If a plugin breaks the site

php artisan zenta:plugins deactivate the-plugin-slug

Or set PLUGINS_SAFE_MODE=true in .env: no plugin is loaded at all until you remove it, and the Plugins screen says safe mode is on. php artisan zenta:plugins lists what is installed.

Writing one

plugins/hello-world/
    plugin.json
    src/HelloWorldServiceProvider.php
    assets/                  served at /plugins/hello-world/assets/…
    database/migrations/     run when the plugin is activated
    resources/views/

plugin.json:

{
    "slug": "hello-world",
    "name": "Hello World",
    "version": "1.0.0",
    "description": "Adds a card to the dashboard.",
    "author": "Your name",
    "namespace": "Acme\\HelloWorld\\",
    "provider": "Acme\\HelloWorld\\HelloWorldServiceProvider"
}

The folder name must match the slug. Classes under the namespace are loaded from src/. The provider is an ordinary Laravel service provider — register routes, views, translations and commands there as you would in any Laravel package — and hooks() is how it joins in with the product:

namespace Acme\HelloWorld;

use Illuminate\Support\ServiceProvider;

class HelloWorldServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'hello');

        // A card on the dashboard.
        hooks()->addAction('admin.dashboard', fn () => view('hello::card')->render());

        // Tell a system of yours whenever an invoice is paid.
        hooks()->addAction('event.invoice.paid', function ($invoice, array $payload) {
            // ...
        });

        // A menu entry of its own.
        hooks()->addFilter('navigation.sections', function (array $sections) {
            $sections['plugins']['label'] = 'Plugins';
            $sections['plugins']['items']['hello'] = [
                'label' => 'Hello', 'icon' => 'emoji-smile', 'route' => 'hello.index',
            ];

            return $sections;
        });
    }
}

Where a plugin can join in

HookKindWhat it is
admin.head, admin.footeractionMarkup returned is added to every staff page — a stylesheet, a script.
portal.head, portal.footeractionThe same for the customer portal.
admin.dashboardactionMarkup returned is added below the dashboard.
eventactionEvery event the automations see: ($event, $record, $payload).
event.{name}actionOne event, e.g. event.invoice.paid: ($record, $payload). The names are the triggers in config/automation.php.
navigation.sectionsfilterThe sidebar, as in config/navigation.php.
settings.sectionsfilterThe list down the side of Settings — add a page for your plugin's own settings.

Use @hook('name') in a Blade view of your own to offer a hook point to other plugins. A listener that throws is logged and skipped, so a bug in one plugin does not take a page down. Automation actions can be added too — see Automations.

A plugin's tables stay when it is deactivated or deleted: dropping data because a folder was removed is not a decision to make for somebody.

Addresses on this page

For reference and for anyone scripting against the panel. Everything here needs somebody signed in to the workspace whose role allows it; anybody else is refused.

MethodAddressWhat it does
GETadmin/settings/pluginsThe installed plugins and the install form. Administrators; in SaaS mode the super admin only.
POSTadmin/settings/pluginsUpload and install a plugin .zip. At most ten a minute.
POSTadmin/settings/plugins/{slug}/activateActivate: switches a plugin on and runs its migrations.
POSTadmin/settings/plugins/{slug}/deactivateDeactivate: switches a plugin off, keeping its data.
DELETEadmin/settings/plugins/{slug}Delete: removes a switched-off plugin's files. Its tables stay.