Package, Install and Update a Plugin

How a finished plugin gets onto a site, how it is upgraded, and how it is removed — with the exact commands and the limits of the Plugin Manager.

1. Build the package

A package is a ZIP whose contents are one module folder. Both layouts are accepted by the validator:

testimonials-1.0.0.zip                 testimonials-1.0.0.zip
  Testimonials/            (a)           module.json            (b)
    module.json                          composer.json
    composer.json                        app/ ...
    app/ ...

PowerShell — build a ZIP of the sample plugin

$src = "examples\Testimonials"            # from the documentation folder
$tmp = Join-Path $env:TEMP "pkg"; Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory $tmp | Out-Null
Copy-Item -Recurse $src "$tmp\Testimonials"
Remove-Item "$tmp\Testimonials\tests" -Recurse -Force
Compress-Archive -Path "$tmp\Testimonials" -DestinationPath testimonials-1.0.0.zip
cd /tmp && rm -rf pkg && mkdir pkg && cp -r /path/to/Testimonials pkg/ && rm -rf pkg/Testimonials/tests
(cd pkg && zip -r ../testimonials-1.0.0.zip Testimonials -x '*.DS_Store')

A ready-made testimonials-1.0.0.zip is available to download. It contains the complete example plugin without the tests.

2. Install

  1. UploadOpen Appearance → Plugins (permission install plugins), choose Add new and upload the ZIP. The archive is validated and unpacked into a private temporary folder; nothing in it is executed. On success it is moved to Modules/{Name} and listed as inactive.
  2. Refresh the autoloaderOn the server run composer dump-autoload (or composer dump-autoload -o in production). This step is required: the Plugin Manager never runs Composer.
  3. ActivateClick Activate (permission activate plugins). Missing, inactive or out-of-range dependencies are reported next to the plugin.
  4. Migrate and seedRun php artisan migrate, and the plugin's seeder if it has one: php artisan db:seed --class="Modules\Testimonials\Database\Seeders\DatabaseSeeder".
  5. Publish assets (if any)php artisan module:publish Testimonials copies resources/assets to public/modules/testimonials.
  6. Clear cachesphp artisan optimize:clear (the Plugin Manager already does this on every operation).

From the command line (developers and CI)

cp -r Testimonials Modules/Testimonials        # or unzip into Modules/
composer dump-autoload
php artisan module:enable Testimonials
php artisan migrate
php artisan module:publish Testimonials         # only if it ships resources/assets
php artisan optimize:clear
php artisan module:list                          # shows Enabled/Disabled and priority

Command-line commands bypass safeguards

module:enable, module:disable and module:delete are plain nwidart commands: they do not check dependencies, protection or locks. The UI path through PluginManager does.

3. Updating a plugin

The upload flow refuses a plugin that is already installed ("Plugin updates are not supported by this upload flow"). Ship an update as a replacement of the files:

  1. Put the site in maintenance mode if the update changes the database (Settings → Maintenance or php artisan down --secret=...).
  2. Replace the folderDeactivate (optional), replace the contents of Modules/{Name} with the new version, keeping any site-specific files.
  3. Autoload and migrate
    composer dump-autoload
    php artisan migrate --force
    php artisan optimize:clear
  4. Bump the version in module.json so dependants that require a version range ("yourplugin": "^1.2") can tell.
  5. Re-run seeders that are idempotent (template-library seeders, default settings) and republish assets.
  6. Bring maintenance mode down and smoke-test.

Rules that make updates painless:

4. Assets in a distributable plugin

The platform's own Vite build lists every bundled module entry in the root vite.config.js; a plugin installed from a ZIP cannot rely on that. Choose one:

ApproachHowBest for
Static assetsPut compiled CSS/JS/images in resources/assets; after install run php artisan module:publish {Name}; reference with asset('modules/{alias}/app.css') in BladeZIP-distributed plugins
Inline in Blade@push('page_header') / @push('page_scripts') with small snippetsTiny additions
Vite entry in the root configAdd the file paths to input in vite.config.js and run npm run build, then @vite('Modules/{Name}/resources/js/app.js')In-repo modules and custom builds
Shortcode assetsDeclare ->assets(['vite' => [...], 'styles' => [...], 'scripts' => [...]]) on the shortcode definition; they load only on pages containing the tagPage-builder blocks

5. Deactivate and remove

6. Release checklist

  1. module.json: valid name, alias, description, version, providers, accurate requires.
  2. composer.json: PSR-4 map for app/, database/factories, database/seeders.
  3. All strings in lang/en/*.php (add the other languages you support).
  4. Every named page route has a breadcrumb; every route is permission-gated; route names follow admin.{alias}.*.
  5. Migrations idempotent, permissions in a migration, a factory for each model.
  6. vendor/bin/pint {Module} and vendor/bin/phpstan analyse {Module} pass; tests pass.
  7. No env() calls outside config/; no hard-coded URLs; no {{ }}/@php in data baked into page content.
  8. Assets built and published, or inlined.
  9. Install the ZIP on a clean site, run the activation steps above, and walk through every screen.