Plugins
Adding features without editing the product: install a plugin from a .zip, or write your own with actions and filters.
Installing one
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.
- Under Install a plugin, choose the Plugin .zip — a
.zipwithplugin.jsonat the top, up to 20 MB — and press Upload and install. It is installed but not switched on. - 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
- Deactivate, after you confirm, switches an active plugin off. Its settings and data are kept, so activating it again brings it back as it was.
- Delete is offered only on a plugin that is switched off. After you confirm, it removes the plugin's files from
plugins/. Any tables it made are left in the database.
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
| Hook | Kind | What it is |
|---|---|---|
admin.head, admin.footer | action | Markup returned is added to every staff page — a stylesheet, a script. |
portal.head, portal.footer | action | The same for the customer portal. |
admin.dashboard | action | Markup returned is added below the dashboard. |
event | action | Every event the automations see: ($event, $record, $payload). |
event.{name} | action | One event, e.g. event.invoice.paid: ($record, $payload). The names are the triggers in config/automation.php. |
navigation.sections | filter | The sidebar, as in config/navigation.php. |
settings.sections | filter | The 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.
| Method | Address | What it does |
|---|---|---|
GET | admin/settings/plugins | The installed plugins and the install form. Administrators; in SaaS mode the super admin only. |
POST | admin/settings/plugins | Upload and install a plugin .zip. At most ten a minute. |
POST | admin/settings/plugins/{slug}/activate | Activate: switches a plugin on and runs its migrations. |
POST | admin/settings/plugins/{slug}/deactivate | Deactivate: switches a plugin off, keeping its data. |
DELETE | admin/settings/plugins/{slug} | Delete: removes a switched-off plugin's files. Its tables stay. |