How the TruCMS Plugin System Works

In TruCMS a plugin is a Laravel module that the in-house Plugin Manager can install, activate, deactivate and delete. Booking, TruCommerce, TruPayments and the four payment gateways are built exactly this way, so everything they do, your plugin can do.

In the user guide: the Plugins chapter.

Module or plugin?

They are the same thing seen from two sides:

Anatomy of a request that involves a plugin

php-fpm starts the app
  bootstrap/providers.php
    ... AppServiceProvider, DotartisanServiceProvider, WidgetsServiceProvider
    LaravelModulesServiceProvider
        reads modules_statuses.json, takes every module that is "true"
        for each, in "priority" order, registers the providers named in module.json
            YourServiceProvider::register()    bindings, config merge
    (all providers registered)
    YourServiceProvider::boot()                views, translations, migrations, routes (child provider),
                                               breadcrumbs, dashboard rows, shortcodes, widgets,
                                               Event::listen(AdminBooting::class, ...)
    $app->booted(...)                          WidgetsServiceProvider loads saved widget instances
                                               FrontendRoutingServiceProvider adds the catch-all slug route

an ADMIN request additionally runs:
  AdminTheme middleware -> core menu + settings registered -> event(AdminBooting)
        -> YOUR listener registers sidebar items and settings sections
  -> your controller -> your Blade view inside <x-admin-layout>

An inactive module is simply never registered: no routes, menus, widgets, schedules or listeners. Its tables are left untouched.

The lifecycle in the Plugin Manager

ActionPermissionWhat happens (class)
Install (upload a ZIP)install pluginsPluginArchiveValidator inspects and safely extracts the archive to a private temporary folder, PluginMetadataReader validates module.json, PluginDependencyResolver checks installation dependencies, then PluginInstaller moves the folder to Modules/{Name}, records "{Name}": false in modules_statuses.json and runs optimize:clear. Nothing from the upload is executed and the plugin is always installed inactive
Activateactivate pluginsChecks every required plugin is installed, active and satisfies its version constraint, flips the flag with $module->enable(), runs optimize:clear. Any failure restores the previous modules_statuses.json snapshot
Deactivateactivate pluginsRefused while an active plugin depends on it or the dependency graph cannot be read; otherwise flag off and optimize:clear
Deletedelete pluginsOnly an inactive, non-protected plugin with no installed dependents. Moves the folder to a private trash folder first (with rollback), removes its status entry, clears caches, then deletes the trash. The database tables are never dropped
Listmanage pluginsPluginManager::listing() returns, per plugin, the metadata, active, protected, and the flags can_activate, can_deactivate, can_delete plus human-readable issues

All operations take a cache lock (plugin-management, 120 seconds, config('plugins.lock_seconds')) so two administrators cannot change plugin state at the same time, and failures are reported as PluginOperationException with a translatable message key (lang/en/plugins.php).

What the Plugin Manager does not do

Dependency rules

Upload limits (config/plugins.php)

SettingDefault
archive.max_upload_kilobytes20 480 (20 MB)
archive.max_files / max_entries2 500 files / 3 000 entries
archive.max_uncompressed_bytes104 857 600 (100 MB)
archive.max_path_length240 characters
temporary_path / trash_pathstorage/app/private/plugin-installer / plugin-trash
lock_seconds120

The validator also rejects: absolute or .. paths, Windows reserved device names, symlinks and special files, duplicate entries (case-insensitively), more than one module.json, a composer.json whose PSR-4 does not map Modules\{Name}\ to app/, and providers that do not exist as files.

What a plugin can do

Everything below is registered from the plugin's service provider; none of it needs a core edit.

CapabilityAPIGuide
Own database tablesMigrations in database/migrationsModels, migrations and media
Admin screens and routesRoute provider + AdminTheme + ->can()Routes, controllers and views
Public pages and URLsFrontTheme routes, public_paths, HasSlugPage types and public URLs
Sidebar entries, dashboard cardsMenuManager, DashboardAdmin menu and dashboard
Settings screensSettingsManagerSettings screens
Permissions and rolesPermission migrations/seedersPermissions and roles
Page-builder blocksShortcode registryShortcodes
Data-bound sectionsBindingSourceRegistryData-bound sections
WidgetsWidgets::register()Widgets and widget areas
Menu-builder link sourcesMenuBuilder::register()Menus and navigation
Page types ("Shop page")AppPages::register()Page types and public URLs
Template-library layoutsModule resources/page-templates + seederTemplate library
Payment gateways, paymentsTruPayments contractsPayments and gateways
Events, jobs, schedules, notificationsListeners, configureSchedules()Events, jobs and schedules
Translationslang/ with namespaceLocalization
Cache invalidationBaseModel, ResponseCacheInvalidatorCaching and performance

Plugin design guidelines

  1. Be additive. Never edit app/ to wire your plugin in. If you find you must, a hook is missing — see Known quirks for the places the core is not fully decoupled yet.
  2. Gate everything. Parent access {alias} permission on the route group (as TruCommerce and TruPayments do) plus a permission per action.
  3. Be cheap when idle. Registrations run on every request. Build field definitions lazily (closures), avoid queries in boot(), and register admin-only things on AdminBooting.
  4. Prefix everything with your alias: routes, permissions, settings keys, widget keys, shortcode ids, dashboard item ids, scheduled commands.
  5. Leave data behind safely. Deactivation and deletion never drop tables; make re-activation idempotent (migrations guard with Schema::hasTable, seeders use updateOrCreate).
  6. Test it like the bundled modules do — see Testing.