Skip to content
Z-CMS is being built in the open on GitHub. Join the community β†’

← All posts

How Z-CMS Loads Plugins Without Restarting the Server

Jul 11, 2026 Β· Z-SOFT Admin Β· 4 min read

A lifecycle-oriented approach to zero-downtime plugin activation and updates

Why restart-based plugin management does not scale

Restarting a server after every plugin installation may be acceptable for a personal website, but it becomes disruptive when a CMS serves many tenants, active sessions, background jobs and real-time connections. Z-CMS treats a plugin as a managed runtime that can be installed, activated, drained, replaced and removed independently.

Dynamic import is only the first step

Dynamic import can load code, but it does not provide safe unloading. Module caches remain, old event listeners can survive, cron jobs may continue and database connections may leak. Hot plugin management is therefore a lifecycle problem rather than a module-loading trick.

The plugin lifecycle

Downloaded
   ↓
Verified
   ↓
Installed
   ↓
Initialized
   ↓
Activated
   ↓
Running
   ↓
Deactivated
   ↓
Uninstalled

Updates introduce an additional sequence: download the new version, verify signatures, validate compatibility, check migrations, start a new runtime, run health checks, switch the active version and finally drain the old runtime.

Manifest-driven initialization

{
  "name": "@z-cms/plugin-seo",
  "version": "1.2.0",
  "main": "dist/index.js",
  "engines": { "z-cms": ">=1.0.0" },
  "permissions": ["content:read", "content:update"],
  "hooks": ["content.beforePublish", "content.afterPublish"]
}

The manifest tells the Core which entry point to load, which CMS versions are supported, which permissions are required and which hooks must be registered.

Creating a new runtime

const runtime = await pluginRuntimeManager.create({
  pluginId: plugin.id,
  version: plugin.version,
  memoryLimitMb: 64,
  timeoutMs: 2_000,
});

await runtime.load(plugin.bundlePath);
await runtime.call("activate", { settings, permissions });

Only after activation and a health check succeed is the runtime allowed to receive production events.

Hook registry instead of hard-coded calls

Hook Registry
β”‚
β”œβ”€β”€ content.beforePublish
β”‚   β”œβ”€β”€ plugin-seo
β”‚   └── plugin-moderation
β”œβ”€β”€ content.afterPublish
β”‚   β”œβ”€β”€ plugin-analytics
β”‚   └── plugin-webhook
└── media.afterUpload
    └── plugin-image-optimizer

Core services dispatch an event to the registry. The registry resolves active plugins and routes execution to the correct isolate. Disabling a plugin removes its registrations immediately without restarting the application.

Managed resources and deactivation

Plugins can create listeners, schedules, subscriptions and temporary resources. The SDK therefore routes registration through managed APIs. Z-CMS records which resource belongs to which plugin and can clean it automatically during deactivation.

export default definePlugin({
  async activate(cms) {
    cms.events.on("content.published", handlePublished);
  },
  async deactivate(cms) {
    cms.events.off("content.published", handlePublished);
  },
});

A well-written deactivate method is still encouraged, but Core cleanup does not rely exclusively on plugin discipline.

Blue-green plugin updates

plugin-seo
β”‚
β”œβ”€β”€ Runtime A β€” 1.2.0 β€” Active
└── Runtime B β€” 1.3.0 β€” Starting

The new version starts alongside the current version. It loads, activates and passes a smoke test before the registry pointer changes. If the health check fails, the old version remains active.

Draining active executions

The old runtime stops accepting new executions but continues handling work already in progress. An active execution counter and a maximum drain timeout prevent requests from being interrupted unnecessarily.

Accept new executions: No
Wait for active executions: Yes
Maximum drain timeout: 30 seconds

Migrations and rollback

Plugin updates may include data migrations. Z-CMS records migration identifiers, creates a recovery point, runs compatibility checks and starts the new runtime before committing the switch. Reversible migrations can be rolled back automatically when activation fails.

{
  "migrations": [{
    "id": "20260714_add_focus_keyword",
    "up": "migrations/001-up.js",
    "down": "migrations/001-down.js"
  }]
}

Versioned storage

/plugins/plugin-seo/
β”œβ”€β”€ 1.2.0/bundle.js
β”œβ”€β”€ 1.3.0/bundle.js
└── active.json

Keeping previous verified bundles enables fast rollback and reproducible incident investigation.

Circuit breakers and development mode

A plugin that repeatedly fails can be moved from Active to Degraded and then Suspended. In development, a file watcher rebuilds the bundle, creates a new isolate, runs activation and swaps runtimes, giving developers a hot-reload experience without restarting the entire backend.

Conclusion

Loading plugins without a server restart requires a complete lifecycle: validation, runtime creation, registry switching, managed cleanup, draining, migration handling, rollback and health monitoring. Z-CMS treats plugin versions as replaceable runtime units so the CMS can remain available while its ecosystem evolves.

GitHub: https://github.com/zscontributor/z-cms
Website: https://z-cms.org