WebBlocks CMS Plugin System

This document records the architecture for the WebBlocks CMS plugin system. CMS core is a generic plugin host with registry-backed plugin definitions, manual super-admin ZIP upload/install, storage-owned install paths, disabled-by-default installed plugins, explicit enable/disable management, manual-upload uninstall, compatibility checks, enabled-only routes and commands, settings/detail scaffolding, health/status reporting, typed admin extension slots, plugin-owned block declarations, public asset hooks, package convention guards, a checksum-verified Plugin Catalog install bridge for compatible public catalog artifacts, and a controlled catalog-backed update action for installed plugins with newer compatible releases. WebBlocks UI Manager is no longer bundled into CMS core runtime; it is an internal/operator plugin artifact installed manually only on operator installs such as webblocksui.com. There is no public marketplace, full remote plugin store, arbitrary Composer package installer, automatic external plugin download/update, automatic external production WebBlocks UI CDN deployment, or generic update-server publishing.

Core Decision

WebBlocks CMS core is a plugin host.

The core package provides the reusable CMS product surface:

  • content and site management
  • public rendering infrastructure
  • user, role, and permission foundations
  • admin shell and standard admin UI surfaces
  • plugin discovery, registry, and extension slot contracts

Product-specific or business-domain-specific capabilities must not be embedded into CMS core unless they are part of the reusable CMS product. They should be delivered as plugins so one install does not inherit another product's menus, commands, settings, data tables, or operational workflows.

Expected plugin areas include:

  • WebBlocks UI Release/CDN Manager
  • QuizTem integration
  • analytics
  • SEO pro tools
  • newsletter
  • commerce
  • media optimizer
  • update server manager
  • custom block packs

Core Vs Plugin Boundary

Core capabilities:

  • sites, pages, blocks, media, users, locales, and base settings
  • rendering, public shell, layout, slot, and block infrastructure
  • permission and role foundation
  • admin shell and standard admin UI surface
  • plugin discovery, registry, and extension slot contracts

Plugin capabilities:

  • admin screens for a specific product or business domain
  • a plugin-owned route namespace
  • plugin-owned permissions
  • plugin-owned settings
  • plugin-owned console commands
  • plugin-owned migrations
  • plugin-owned dashboard widgets
  • plugin-owned blocks or block packs
  • plugin-owned public routes, only when explicitly declared

Core view override is forbidden by default. Plugins extend CMS only through documented extension slots and registry contracts. A plugin must not replace package views, monkey patch core services, add hidden route files, or rely on arbitrary include side effects.

Manual ZIP Install

System -> Plugins lets super admins upload a local plugin ZIP. Uploading a ZIP is privileged executable-code installation. The installer validates the archive before writing anything under the configured plugin root, defaulting to storage/app/webblocks/plugins/{plugin-handle}/{version}.

Validation requires webblocks-plugin.json or manifest.json, a kebab-case handle, a semver-like version, provider/class metadata, a compatible CMS version constraint, no installed handle collision, relative package paths only, no path traversal, no absolute paths, no symlink entries, and no writes to forbidden CMS/core targets such as app, packages, project, storage, vendor, or public/cms. Installed plugins remain disabled unless an explicit enable step is completed. Disabled plugins are inert: routes, commands, menus, settings routes, health reporters, widgets, block declarations, and assets are not registered or executed, and System -> Plugins reports health as inactive/not checked.

Manual uninstall is available only for manually uploaded plugins and requires super-admin authorization. The plugin must be disabled first. Uninstall removes the installed-plugin package directory and enabled-state file under the configured plugin root, but it does not drop plugin-owned database tables or run destructive migrations. Protected/core/non-manual plugins cannot be uninstalled through this flow.

Supported manifest fields include handle, label, description, version, provider, required_cms_version, permissions, commands, routes, settings, migrations, assets, and health. Since CMS 1.92.0, installation, updates, and activation run required plugin database changes automatically. Execution stays scoped to the installed plugin and its declared migration directories; unrelated host migrations are excluded. A failed database update leaves the plugin disabled while preserving its tables. The plugin detail screen reports pending or failed setup and offers an explicit retry. Repair can rerun recorded migrations when required plugin tables are missing.

Plugin lifecycle on CMS 1.94.2 is:

  1. Upload/install the ZIP. The CMS checks compatibility and startup, then applies required database changes. A new plugin remains disabled by default.
  2. Review the plugin detail screen and its setup result.
  3. Enable the compatible plugin. Activation verifies startup and completes required setup before making the plugin available.
  4. If setup failed, inspect the pending or failed state and retry from the plugin detail screen; the plugin stays disabled until setup succeeds.
  5. Use operational plugin routes after setup is ready.
  6. Disable the plugin to make routes, commands, menus, settings, health checks, and contributions inert.
  7. Uninstall only after disable; uninstall preserves plugin-owned tables.

Enabled plugins with pending setup must not crash admin routes. If plugin-owned tables are missing, health and route screens should report setup-required guidance such as Plugin migrations pending or Release tables are missing.

Plugin admin routes should use declared handle-prefixed permissions. CMS super_admin users are explicitly allowed for active enabled-plugin permissions, including permissions loaded from manually installed plugin manifests. Non-super-admin roles remain denied unless a future CMS permission assignment model grants the specific plugin-owned permission. Settings routes use {plugin-handle}.manage when the plugin declares it, otherwise they fall back to system access.

Enabled compatible plugin admin routes always run inside the CMS admin route stack: web, install-required, CMS auth, CMS admin access, plugin setup guard, and then plugin-owned permission middleware when the plugin route declares it. The CMS auth middleware uses Laravel's web guard/session user while redirecting guests through the CMS-owned webblocks.auth.login route. Plugin setup guards are additive and must not replace CMS authentication or admin authorization; plugin controllers can rely on the authenticated CMS user being present after the auth/admin middleware has passed.

Startup Validation And Recovery (1.94.0–1.94.2)

CMS 1.94.0 adds mandatory startup validation to ZIP/catalog installation, updates, and panel/API activation. The candidate runs in a fresh PHP process, even if it declares no database migrations. The check loads its source, provider, commands, and admin/API/public routes; invalid PHP, missing classes, startup or route failures, early process exit, and timeouts reject the candidate. The default timeout is 30 seconds, configured by webblocks-plugins.install.boot_timeout_seconds.

From 1.94.1, the isolated check treats the candidate as enabled while keeping other installed plugins unloaded, including providers that only do work when enabled. A rejected update keeps the current working package. Successful updates retain the previous package and record whether database migrations ran. Failed database setup disables the plugin and preserves its data and packages; lifecycle records are written atomically and stale update requests cannot replace a newer installed version.

Runtime failures while loading CMS-managed plugin source or registering routes quarantine the failing plugin, remove partially registered routes, and keep core pages and other plugins available. Subprocess output and exception details are excluded from operator-facing notices and lifecycle records. This is a recovery mechanism for managed plugins, not a sandbox for arbitrary executable PHP or host application providers.

Recover Without SSH

  1. Open /webadmin/plugin-recovery. The recovery screen and its login form load without installed plugin source, routes, or commands.
  2. Sign in with an active Super admin account that can access the admin panel. CMS 1.94.2 also applies the normal admin-access check to existing sessions; inactive accounts cannot view recovery information or change plugin state.
  3. Disable the failing plugin, or restore its retained previous package when the recovery screen reports that no database migration ran during the update.
  4. A restore repeats the startup check and republishes the retained version's static assets. It does not roll back database schema or data.

Normal login controls and CSRF protection apply. A plugin that terminates PHP outright may still require opening the recovery URL in a separate request.

Plugin Contract And Manifest

Every plugin must have a handle:

  • kebab-case
  • globally unique within the install
  • stable across releases
  • used as the default prefix for permissions, routes, tables, settings, assets, and package identity

Each plugin should declare metadata through a manifest or definition object:

  • handle
  • label
  • version
  • provider class
  • optional description
  • required CMS version or version constraint
  • settings namespace
  • database/table prefix
  • permissions
  • admin menu entries
  • admin and public routes
  • console commands
  • settings schema or settings pages
  • migrations
  • blocks or block packs
  • assets
  • health checks, when supported

Plugins are registry-first. They connect to CMS through explicit contracts rather than random includes, root view overrides, or install-specific route files.

Catalog-updated installed plugins are reloaded through CMS core runtime refresh after the replacement package is written. The refresh clears plugin registry, permission, extension, health, and optimized Laravel runtime cache state, then rebuilds active plugin routes for the current runtime so the installed version, active manifest version, active provider metadata, and route/controller source path stay aligned. If a provider class from an older installed package version is already loaded in the same PHP process, CMS treats that provider as stale and falls back to the updated manifest metadata instead of reusing old route paths.

Plugin authorization is resolved centrally by CMS core. Active plugin-owned permissions declared by the enabled plugin allow CMS super_admin users, and the same resolver is used for plugin menu visibility, dashboard/system contributions, and plugin.permission:* route middleware. Unauthorized users do not see matching plugin menu entries; direct URL access remains a controlled 403. Setup-required handling remains after authorization, and plugin migrations are still explicit.

Current registry API shape:

PluginDefinition::make('webblocks-ui-manager')
  ->label('WebBlocks UI Manager')
  ->version('1.0.0')
  ->requiresCms('^1.32')
  ->provider(WebBlocksUiManagerServiceProvider::class)
  ->settingsNamespace('webblocks_ui_manager')
  ->databasePrefix('webblocks_ui_manager_')
  ->menu([
    PluginMenuItem::make('releases')
      ->label('WebBlocks UI Releases')
      ->icon('package')
      ->route('webblocks.plugins.webblocks_ui_manager.releases.index')
      ->permission('webblocks-ui-manager.view'),
  ])
  ->permissions([
    PluginPermission::make('webblocks-ui-manager.view')->label('View releases'),
    PluginPermission::make('webblocks-ui-manager.manage')->label('Manage release metadata'),
    PluginPermission::make('webblocks-ui-manager.publish')->label('Prepare CDN artifacts'),
  ])
  ->adminRoutes(__DIR__.'/../routes/admin.php')
  ->commands([
    PrepareWebBlocksUiReleaseCommand::class,
  ])
  ->settings(
    PluginSettingsDefinition::make()
      ->label('Release Settings')
      ->description('Controls WebBlocks UI release publishing defaults.')
  )
  ->dashboardWidgets([
    PluginDashboardWidget::make('webblocks-ui-manager.release-status')
      ->title('WebBlocks UI Releases')
      ->description('Read-only release publishing summary.')
      ->permission('webblocks-ui-manager.view'),
  ])
  ->systemCards([
    PluginSystemCard::make('webblocks-ui-manager.cdn-status')
      ->title('CDN Status')
      ->description('Read-only CDN artifact status.')
      ->permission('webblocks-ui-manager.view'),
  ])
  ->blockTypes([
    PluginBlockTypeDefinition::make('webblocks-ui-manager::release-card')
      ->label('Release Card'),
  ])
  ->publicAssets([
    PluginPublicAsset::cssHead('webblocks-ui-manager.public-css', '/cms/plugins/webblocks-ui-manager/public.css'),
    PluginPublicAsset::jsBodyEnd('webblocks-ui-manager.public-js', '/cms/plugins/webblocks-ui-manager/public.js'),
  ])
  ->health(WebBlocksUiManagerHealth::class);

The exact API may change during implementation, but the contract must preserve these rules:

  • declared metadata is inspectable before a plugin is enabled
  • menu, route, permission, command, migration, block, asset, and setting ownership is attributable to a plugin handle
  • conflicts fail during build, test, boot diagnostics, or plugin enablement before users see mixed ownership

Package Convention Rules

Plugin package conventions are separate from CMS core conventions. CMS core owns host contracts; plugins own their domain behavior.

  • Handle naming: use stable kebab-case such as analytics-tools; never rename a handle after release because it anchors routes, permissions, settings, tables, assets, and upgrade history.
  • Service provider registration: a plugin package should expose one Laravel service provider and register its PluginDefinition there or through the CMS registry integration point. First-party in-package pilots may register directly from the CMS package provider until split into their own Composer packages.
  • Definition or manifest structure: declare handle, label, version, provider, description, requiresCms, settings namespace, database prefix, permissions, routes, commands, extension slots, assets, blocks, and health reporter explicitly.
  • Route namespace: admin routes live under /webadmin/plugins/{plugin-handle}/... with names under webblocks.plugins.{plugin_handle}.*.
  • Permission naming: every plugin permission starts with {plugin-handle}., for example analytics-tools.view.
  • Settings conventions: settings namespaces are snake_case and default to the handle with dashes converted to underscores.
  • Command naming: resolvable Artisan command names must start with {plugin-handle}:, for example analytics-tools:sync.
  • Migration and table naming: tables use a registry-reserved snake_case prefix ending in _, defaulting to the handle converted to snake_case plus _.
  • Asset contributions: public asset handles are dot-namespaced with the plugin handle, and static files must publish under a plugin-owned path.
  • Dashboard and system card contributions: keys are dot-namespaced with the plugin handle and remain read-only unless a later extension contract adds editable behavior.

WebBlocks UI Manager follows these conventions as the first-party pilot: handle webblocks-ui-manager, settings namespace webblocks_ui_manager, database prefix webblocks_ui_manager_, commands webblocks-ui-manager:prepare-release and webblocks-ui-manager:publish-release, routes under /webadmin/plugins/webblocks-ui-manager, and route names under webblocks.plugins.webblocks_ui_manager.*.

Compatibility And Inertness

Plugin versions are semver-like metadata. requiresCms() declares the CMS version constraint needed by the plugin. The current foundation supports exact/comparator constraints such as >=1.32.0 and caret constraints such as ^1.32.

The registry distinguishes configured enabled state from active state:

  • Configured enabled: config/webblocks-plugins.php says the plugin should be enabled.
  • Compatible: the installed CMS version satisfies the plugin's required CMS constraint.
  • Active: the plugin is both configured enabled and compatible.

Only active plugins contribute menus, routes, commands, settings routes, dashboard widgets, system cards, block declarations, public assets, permissions, and health reporter execution. Disabled and incompatible plugins remain inert. System -> Plugins displays Incompatible with the required and installed CMS versions when a configured plugin cannot activate.

Discovery And Local Enablement

Phase 5 does not add marketplace behavior or arbitrary remote installation. Safe discovery is local and explicit:

  • first-party package-owned plugins can be registered by the CMS package provider
  • future Composer package plugins should register a service provider through Laravel package discovery or explicit app provider configuration
  • install-local experiments can use local Composer path repositories during development, but they must still register a normal provider and definition
  • enablement remains config-backed through webblocks-plugins.enabled.{plugin-handle}

No runtime feature installs arbitrary Composer packages, publishes marketplace catalogs, writes production CDN/update-server artifacts, auto-enables newly installed plugins, or performs unattended plugin updates. Required plugin database setup is automatic within an operator-initiated install, update, or activation. Remote plugin artifact bridges are limited to super-admin initiated Plugin Catalog install/update actions described below, and both are limited to controlled catalog ZIP URLs with matching SHA-256 metadata.

Ecosystem Catalog Direction

The CMS plugin system is the first implementation host for a broader WebBlocks plugin ecosystem direction. Future plugin contracts should be reusable by WebBlocks CMS, QuizTem, Herne Panel, WebBlocks Publisher, and later WebBlocks products where each host exposes its own product-specific extension points.

The proposed catalog/store surface is plugins.webblocksui.com. The near-term target is a Plugin Catalog for discovery, metadata, compatibility, documentation, release information, checksums, controlled ZIP download links, and a conservative install bridge from trusted catalog artifact metadata. Marketplace behavior, including accounts, licensing, paid plugins, reviews, and approval workflows, is deferred.

System -> Plugins includes a Browse Plugin Catalog action at /webadmin/plugins/catalog (admin.plugins.catalog.index). The catalog list requests public listed plugins for host_product=webblocks-cms from GET /api/plugins and asks for the latest compatible release through GET /api/plugins/{handle}/latest when a handle is available. Catalog plugin labels and the View details action open /webadmin/plugins/catalog/{handle} (admin.plugins.catalog.show), which requests GET /api/plugins/{handle} plus the latest compatible release endpoint to show plugin metadata, compatibility, release notes, documentation/support links, artifact download URL, SHA-256 checksum, artifact filename, artifact size, release status, artifact validation status, artifact scan status, channel, version, and safe declared capability metadata when returned by the API. Current WebBlocks Plugins API detail responses may return plugin data directly under data, release data under data.latest_release, and artifact metadata under data.latest_release.artifact; latest-compatible API responses may also return release metadata under data.release with artifact metadata under sibling data.artifact. CMS normalizes those shapes before rendering details, checking install availability, or processing the server-side catalog install action. Supported artifact fields include file_name, size_bytes, checksum_sha256, download_url, validation_status, and scan_status; older flat release field names remain accepted for compatibility. The canonical WebBlocks CMS product version is sent as version and cms_version for compatibility matching. The built-in public catalog URL defaults to https://plugins.webblocksui.com; operators do not need .env changes for default discovery, and can override the internal request target with WEBBLOCKS_PLUGIN_CATALOG_BASE_URL (webblocks-plugins.catalog.base_url). Timeout settings are available through WEBBLOCKS_PLUGIN_CATALOG_TIMEOUT_SECONDS and WEBBLOCKS_PLUGIN_CATALOG_CONNECT_TIMEOUT_SECONDS. Normal catalog UI does not expose the configured base URL or request version; unavailable states use friendly operator copy while safe diagnostics remain in logs.

Catalog detail keeps Website, Documentation, Support, and Catalog Detail links separate from install actions. The page shows a clear unavailable artifact state when a compatible release does not include downloadable artifact metadata. The Download ZIP action opens only the controlled absolute public download_url returned by the catalog and must not expose raw storage paths.

Install from Catalog is available only when the catalog plugin is compatible, the latest compatible release is published, and the normalized release artifact includes download_url, checksum_sha256, and file_name values. Older flat sha256/checksum_sha256 and filename/artifact_filename release fields remain accepted for compatibility. The POST action uses CSRF, re-reads catalog metadata server-side, downloads the controlled ZIP URL to temporary storage, checks HTTP success, rejects unreasonable/non-ZIP responses, calculates SHA-256, compares it exactly with catalog metadata, and then passes the temp ZIP through the existing manual plugin ZIP validator/installer. Temporary files are removed after success or failure, and catalog/remote filenames are not trusted as filesystem paths.

Catalog installs register new plugins disabled by default, the same as manual upload. From CMS 1.92.0, required database setup runs during installation; from 1.94.0, candidate provider startup, commands, and routes are also validated in an isolated process. Installation does not activate the new plugin in the normal runtime. Any local installed/enabled state shown on the catalog detail page comes only from the CMS plugin registry, not from remote catalog claims. Manual ZIP upload/install remains available and unchanged.

System -> Plugins -> Registered Plugins performs a best-effort catalog availability lookup for installed plugin handles. When the catalog is unavailable or does not provide trusted metadata, the list still renders and no update action is shown. When an installed handle has a newer latest compatible release by version_compare, that release is published, the plugin is compatible, and the normalized artifact includes download_url, checksum_sha256, and file_name, the Version column shows Update available: {version} and the row action group shows a POST-only Update from Catalog icon action.

The update POST re-reads catalog detail/latest metadata server-side, requires the same compatible published complete artifact metadata, downloads the controlled ZIP, verifies SHA-256, validates the ZIP through the same plugin package validator, and replaces the installed plugin package version. The plugin-owned database tables are preserved, enabled or disabled lifecycle state is preserved by moving enabled state to the new version only when the old version was enabled, and required plugin database changes run automatically from CMS 1.92.0 onward. No runtime registry, route, health or asset refresh runs after source replacement in that same PHP request: its old plugin classes may already be loaded, so the redirected request performs normal discovery from one consistent package version. If setup fails, the plugin remains disabled and the detail screen provides pending/failed status and a retry action. A failed startup check rejects the candidate before replacing the working package.

See WebBlocks Plugin Ecosystem And Catalog for the product-level direction and phase plan.

Planning for the proposed plugins.webblocksui.com product surface, MVP scope, candidate implementation models, public catalog pages, operator surfaces, and possible read-only API shape lives in Plugin Catalog Product Architecture.

Catalog Update Notes (1.92.4)

The update confirmation presents the release summary and changes as separate readable items. Structured release highlights take precedence; plain notes retain their lists and wrapped lines without repeating the summary or duplicate items. The modal keeps its heading and action footer visible while long notes scroll. Review the current and offered versions before starting the update.

Minimal Plugin Example

A minimal plugin package should expose a provider and a definition similar to:

final class AnalyticsToolsPlugin
{
  public static function definition(): PluginDefinition
  {
    return PluginDefinition::make('analytics-tools')
      ->label('Analytics Tools')
      ->version('0.1.0')
      ->provider(AnalyticsToolsServiceProvider::class)
      ->requiresCms('^1.32')
      ->settingsNamespace('analytics_tools')
      ->databasePrefix('analytics_tools_')
      ->permissions([
        PluginPermission::make('analytics-tools.view')->label('View analytics tools'),
      ])
      ->adminRoutes(__DIR__.'/../routes/admin.php')
      ->commands([
        SyncAnalyticsCommand::class,
      ])
      ->health(AnalyticsToolsHealth::class);
  }
}

The provider should register package views, config, migrations, and the plugin definition without adding CMS-owned /admin, /cms, or root public route files. Plugin routes should be defined relative to the plugin route group; for example, /reports becomes /webadmin/plugins/analytics-tools/reports.

Admin Menu Rules

Plugins may add admin menu entries, but every plugin menu entry must be permission-gated.

The preferred behavior is to add items to existing admin groups such as:

  • System
  • Tools
  • Integrations

A top-level plugin menu may be reserved only for a large product surface that would be confusing as a single group item.

Admin menu rules:

  • icons must come from the WebBlocks UI icon catalog
  • route names must live under the plugin route namespace
  • menu ordering and collision rules must be managed by the registry
  • disabled or uninstalled plugins must not render menu entries
  • menu item labels should describe the capability, not leak install-specific project names into generic CMS installs
  • menu entries must not appear in core installs when the owning plugin is absent

Declare a default sidebar icon with a WebBlocks UI catalog slug in the plugin manifest:

"icon": "calendar"

Composer/provider definitions can declare the same default with PluginDefinition::icon('calendar'). A manifest default takes precedence when a ZIP package declares both. An explicit manifest icon must exist in the icon manifest bundled with the installed CMS. Packages that declare admin menu entries must provide a default icon or a menu icon; existing manifests that only declare PluginMenuItem::icon('wb-icon-calendar') remain supported. Their first menu icon supplies the default. If an icon is unavailable or disabled in the Icons catalog, the sidebar falls back to the generic plugin icon.

A plugin-owned sidebar heading uses that default automatically. Core headings keep their own icons, and a custom heading shared by several plugins keeps a neutral plugin icon.

System administrators can change the icon through the Menu appearance card on the plugin settings page or System → Plugins → plugin details. This reuses the existing icon picker and active Icons catalog. The card has its own save form, independent of the plugin settings. Clear the selection in the picker and save to restore the plugin default.

The preference applies to the whole CMS installation and all operators. It is stored separately from the plugin package, so upgrades preserve it. It changes the plugin-owned heading only; menu item icons, core headings, and shared headings retain their own meaning. Changing the preference requires system access; ordinary plugin operators do not receive installation-wide appearance authority.

Route Namespace Rules

Admin plugin routes default to this URL prefix:

/webadmin/plugins/{plugin-handle}/...

Admin plugin route names default to this namespace:

webblocks.plugins.{plugin_handle}.*

The route name namespace uses the plugin handle transformed only as needed for Laravel route names. For example, webblocks-ui-manager becomes webblocks.plugins.webblocks_ui_manager.* if underscores are required by implementation.

A plugin may request a shorter admin prefix only through the registry. Reserved short prefixes must be globally unique. Prefix conflicts must fail during build, test, boot diagnostics, or plugin enablement.

Plugins must not pollute:

  • CMS core route names
  • the /webadmin core route namespace outside their reserved plugin prefix
  • the legacy /admin namespace
  • the /cms static asset namespace

Disabled and incompatible plugins must not register admin routes. The active-only registrar is intentionally conservative: if a plugin is disabled through config/webblocks-plugins.php or does not satisfy its CMS version constraint, its routes are absent rather than present-but-forbidden.

Public routes are opt-in. A plugin that declares public routes must declare ownership clearly enough that route ownership can be tested. Public plugin routes must avoid collisions with site pages, CMS public routes, and host product routes.

Permission Rules

Every admin menu, route, and action must be attached to a plugin permission.

Permission names must include the plugin handle prefix:

webblocks-ui-manager.view
webblocks-ui-manager.publish
webblocks-ui-manager.settings

Permission behavior must stay compatible with the CMS permission model. If super admin bypass exists, it must use the same explicit CMS authorization path as core CMS permissions.

Plugin permissions must be visible in admin role management when the plugin is installed or discoverable. Disabled plugin permissions must not authorize active behavior, even if a role still stores a matching permission string.

Settings Rules

Plugin settings must be stored in the plugin's own namespace. They must not collide with general CMS config, host application config, or environment variables.

Settings rules:

  • setting keys should be prefixed by the plugin handle
  • sensitive values must use secret-safe storage where available
  • sensitive values must never be rendered in logs, health check output, exception messages, or admin flash messages
  • settings UI must live under the plugin route namespace or inside System -> Plugins -> Plugin detail
  • environment variables may seed defaults, but runtime settings should remain plugin-owned and inspectable through the registry

Phase 2 provides a read-only settings route foundation for enabled plugins that declare PluginSettingsDefinition without a custom route name. The default route is:

/webadmin/plugins/{plugin-handle}/settings

Its default route name is:

webblocks.plugins.{plugin_handle}.settings.edit

Editable settings storage and validation schemas are reserved for a later phase. Phase 5 reserves settings namespaces through PluginDefinition::settingsNamespace() so plugins do not collide with CMS core config or other plugins.

Migration And Data Lifecycle Rules

Plugin migrations must not collide with core migrations.

Plugin table names must carry the plugin handle prefix or a documented shortened prefix reserved by the plugin registry. For webblocks-ui-manager, table names use webblocks_ui_manager_. Phase 5 reserves database prefixes through PluginDefinition::databasePrefix() and rejects duplicate prefixes.

Lifecycle states must be distinct:

  • Enable: plugin menu, routes, commands, scheduled jobs, widgets, blocks, settings, health checks, and actions become available according to permissions and compatibility.
  • Disable: plugin menu, scheduled jobs, plugin routes, plugin actions, settings routes, health checks, widgets, blocks, and assets are unavailable; data remains in place.
  • Uninstall: manually uploaded disabled plugins can be removed from the storage-owned install root. Plugin-owned database tables and historical data remain in place.
  • Uninstall: reserved for a future design; by default it must not delete data.
  • Decommission or purge: future destructive data deletion flow requiring explicit destructive confirmation.

Uninstall must not be database-destructive. Deleting plugin tables, artifacts, uploaded files outside the plugin package directory, or historical records requires a separate explicit destructive confirmation design.

Schema upgrades should be additive and reversible where practical. A plugin release that changes schema must document:

  • minimum compatible CMS version
  • plugin version introducing the schema
  • migration/table prefix used
  • whether disabled plugins can safely leave existing data in place
  • operational notes for rollback or decommission

The manual plugin migration runner is intentionally scoped. It runs only migration directories declared by the installed plugin and only after resolving those directories inside the configured plugin install root. It does not run host app migrations or unrelated plugin migrations. Plugin migrations should be additive, reversible where practical, and safe to retry when setup repair is required.

Assets And Static File Rules

Plugin assets must be published under their own namespace. They must not mix with core public/cms assets.

For WebBlocks UI Manager, versioned CDN output should use immutable paths such as:

public/cdn/webblocks-ui/v2.7.9/...

Asset rules:

  • versioned artifact directories are immutable
  • old versioned directories must not be deleted as part of normal publish
  • latest must not be used for first-party CDN consumption
  • dry-run publish must report writes, skips, and blocked operations without writing files
  • apply publish must validate expected dist files, source paths, release version, target paths, checksums, and manifest consistency before writing
  • existing files with matching checksums are skipped; existing files with different checksums block the run
  • CDN or static hosting should be served by Nginx or another static service when possible
  • Laravel route-based asset streaming must not be the default for CDN files
  • plugin admin assets must be isolated from core CMS admin assets and publish under a plugin namespace

The WebBlocks UI Manager local publish workflow writes only into the configured project-owned static target, defaulting to public/cdn/webblocks-ui/{version}/.... It does not deploy to external production infrastructure, publish update-server metadata, change CMS core WebBlocks UI consumption URLs, or install remote packages.

Phase 3 adds registry-backed public asset declarations for enabled plugins. These declarations are currently limited to explicit asset URLs and are rendered as public page assets only when the owning plugin is enabled:

  • head CSS renders as <link rel="stylesheet"> in the public <head>
  • head JS renders as deferred or async/module <script> tags in the public <head>
  • body-end JS renders near the end of the public <body>
  • asset handles must be dot-namespaced with the plugin handle, such as analytics-tools.public-js
  • disabled plugin assets are absent from collection and rendering

This is an asset contribution hook foundation, not a plugin package installer or asset publisher. Plugins remain responsible for publishing their own static files under a plugin-owned namespace.

Event, Hook, And Extension Slot Rules

Plugins must not monkey patch or override core. Core extension slots must be explicit, documented, and testable.

Initial extension slot candidates:

  • admin.menu
  • admin.dashboard.widgets
  • admin.system.cards
  • permissions.registry
  • block.registry
  • public.head.assets
  • public.body_end.assets

Slot contracts should be typed and value-object based. Avoid raw array contracts where possible so collisions, invalid shapes, and ownership can be validated early.

Phase 3 implements these typed extension-slot objects:

  • PluginDashboardWidget for read-only dashboard cards
  • PluginSystemCard for read-only system cards or links
  • PluginBlockTypeDefinition for plugin-owned block type declarations
  • PluginBlockPackDefinition for grouped plugin block declarations
  • PluginPublicAsset for public head and body-end asset declarations
  • PluginAdminExtensionRegistry, PluginBlockRegistry, and PluginPublicAssetRegistry for enabled-only collection

Dashboard widget and system card keys must be dot-namespaced with the plugin handle, for example analytics-tools.overview. Public asset handles follow the same dot namespace rule. Plugin block handles must use a plugin-owned namespace such as analytics-tools::score-card; unqualified core-style block handles such as hero are rejected. These hooks make plugin contributions discoverable and attributable without replacing core package views.

Dashboard widgets render on the super-admin dashboard only when the plugin is enabled and the current user can satisfy the widget permission, if one is declared. System cards render only on their intended system overview surfaces under the same enabled and permission checks. The System -> Plugins management page does not render generic plugin contribution cards; it stays focused on manual plugin install, lifecycle, health, setup, settings, and uninstall actions unless a future extension slot is explicitly designed for plugin management. Both slots are intentionally read-only foundations.

Block hooks are declaration-only foundations. They let enabled plugins expose plugin-owned block types and block packs through the registry, but they do not replace core block contracts, core block views, core block seeders, or block editing services.

Plugin Lifecycle

The full lifecycle target:

  1. discover
  2. install
  3. enable
  4. disable
  5. health/status
  6. upgrade
  7. uninstall or decommission, in a later destructive-data design

The implemented Phase 1 through Phase 5 runtime target is intentionally smaller than the full lifecycle:

  • registry
  • enabled configuration
  • System -> Plugins listing
  • System -> Plugins detail and read-only settings surfaces
  • admin menu registration
  • permission registration
  • enabled-only admin route registration
  • enabled-only command registration
  • basic health/status reporting
  • typed read-only dashboard and system card extension slots
  • plugin-owned block and block pack declaration hooks
  • public head and body-end asset contribution hooks
  • first-party WebBlocks UI Manager pilot plugin with release metadata, safe local manifest preparation, and controlled local CDN dry-run/apply publishing
  • plugin version and required CMS compatibility metadata
  • incompatible-plugin active-state and health reporting
  • package convention and collision guards
  • route ownership guards

That foundation gives CMS a safe host boundary before plugins gain deeper lifecycle behavior.

Testing And Release Guardrails

The plugin system must be protected by route ownership, package boundary, and coexistence tests.

Required guardrails:

  • plugin route ownership is testable
  • core CMS installs show no plugin menus when no plugins are enabled
  • disabled plugin menus do not render
  • disabled plugin routes and actions are unavailable or fail authorization
  • disabled plugin widgets, system cards, block declarations, and public assets are absent
  • incompatible plugin routes, commands, menus, permissions, widgets, system cards, block declarations, public assets, settings routes, and health reporter behavior are absent
  • command names, database prefixes, handles, extension slots, widgets, blocks, assets, and permission namespaces remain collision-guarded
  • plugin dashboard/system extension cards render only when enabled and permitted
  • plugin-owned block declarations are discoverable without overriding core block contracts
  • plugin public assets are collected by safe location and are absent when disabled
  • plugin route registration must not restore a CMS-owned /admin namespace
  • /webadmin remains the canonical CMS admin prefix
  • /cms remains static asset territory, not a Laravel plugin route namespace
  • plugin attempts to override core tables, route names, or views should fail tests or diagnostics
  • package boundary tests expand to cover plugin-owned routes, views, assets, migrations, and commands
  • coexistence tests should cover CMS + QuizTem + plugin scenarios in the roadmap

Plugin tests should include both absent-plugin and disabled-plugin cases so core CMS remains clean in generic installs. The WebBlocks UI Manager pilot also carries migration/schema, command, manifest/checksum, enabled-only, disabled-inert, admin rendering, route guard, and package boundary tests.

WebBlocks UI Manager Pilot Plugin Decisions

WebBlocks UI Manager is not embedded in CMS core behavior. It currently starts as a first-party package-owned pilot plugin under the CMS package namespace so the plugin host can prove a real product-specific operational surface without moving WebBlocks UI release/CDN management into generic CMS core.

The preferred long-term model may still become a separate Composer package or separate repository after plugin lifecycle and packaging conventions mature.

The plugin's responsibility:

  • WebBlocks UI release artifact records
  • source dist validation
  • safe local publish preparation
  • controlled local static publish dry-runs and apply runs
  • publish run history
  • manifest and checksum generation
  • CDN health checks

The WebBlocks UI build stays in the WebBlocks UI repository. The plugin does not build WebBlocks UI. It receives or validates release artifacts, records local metadata for first-party CDN paths, and can publish validated files into the configured local/project-owned static target. External production CDN deployment is intentionally deferred and must remain explicit.

Our own products may consume cdn.webblocksui.com for pinned first-party assets. External user documentation should continue to recommend GitHub or jsDelivr CDN consumption unless that policy changes separately.

CDN rules for the pilot:

  • use versioned paths
  • do not use latest
  • never mutate an existing versioned artifact directory
  • do not delete old versioned CDN directories during normal publish
  • run dry-run before apply when operating manually
  • block checksum mismatches instead of replacing files silently
  • prefer static serving over Laravel route streaming

WebBlocks UI Manager Publish Flow

Prepare release metadata and checksums:

php artisan webblocks-ui-manager:prepare-release v2.7.9 --artifact=/path/to/webblocks-ui.css --artifact=/path/to/webblocks-icons.css --artifact=/path/to/webblocks-ui.js

Dry-run the publish:

php artisan webblocks-ui-manager:publish-release v2.7.9 --dry-run

Apply the local publish:

php artisan webblocks-ui-manager:publish-release v2.7.9

The admin release detail screen exposes the same dry-run and publish actions when the plugin is enabled, compatible, and the user has webblocks-ui-manager.publish. The real publish action uses a confirmation modal. Dry-run is non-destructive.

Required settings:

  • WEBBLOCKS_UI_MANAGER_ENABLED=true enables the plugin.
  • WEBBLOCKS_UI_MANAGER_CDN_BASE_PATH=cdn/webblocks-ui controls the local/project-owned static root under public/.
  • WEBBLOCKS_UI_MANAGER_CDN_BASE_URL is optional display metadata for generated public URLs.
  • webblocks-plugins.webblocks_ui_manager.expected_dist_files lists required dist filenames.

Publish validation checks:

  • release metadata exists and is not draft
  • release version is semver-like
  • release CDN path equals the configured root plus the release version
  • expected dist files are present
  • source files exist inside the project root and are not symlink escapes
  • artifact target paths stay inside the configured CDN root
  • stored artifact checksums match current source files and manifest metadata
  • existing published files either match checksums and are skipped or block the run
  • existing manifest content must match prepared manifest content

Publish runs are stored in webblocks_ui_manager_publish_runs with mode, status, target paths, operation details, and secret-free failure messages.