Instatic Plugin Lifecycle: Complete Guide to Install, Activate, and Uninstall Hooks

The Instatic plugin lifecycle follows five deterministic phases—install, activate, deactivate, migrate, and uninstall—where the host coordinates sandboxed execution via exported hooks and persists state in the installed_plugins table.

The Instatic platform, developed by CoreBunch, treats every plugin as a self-contained zip package that runs inside a secure sandbox. Understanding the Instatic plugin lifecycle is essential for developers building extensions, as the host manages each transition through specific exported functions while guaranteeing rollback safety and crash recovery.

The Five Lifecycle Phases

The host coordinates each phase through the runtime system implemented in server/plugins/runtime.ts and tracks status via the lifecycleStatus column in the database. Each phase offers an optional hook that plugins can export to execute custom logic.

Install Phase

During installation, the host validates the plugin.json manifest against the schema defined in src/core/plugins/manifest.ts, checks permission coherence, and extracts the zip archive to uploads/plugins/<id>/<version>/. If the plugin exports an install hook, the host executes it inside a temporary sandbox context.

export function install(api) {
  // Create initial collections, seed data, etc.
  const coll = api.cms.storage.collection('approvals')
  return coll.create({ title: 'Initial approval' })
}

Upon successful completion, the lifecycleStatus is set to installed, indicating the package exists on disk and the hook succeeded without errors.

Activate Phase

Activation transforms an installed plugin into a running service. The host starts a Bun Worker and creates a QuickJS-WASM VM sandbox, then calls the activate(api) hook. This is where plugins register routes, hooks, scheduled tasks, and settings.

export function activate(api) {
  // Register a route and a hook
  api.cms.routes.get('/status', 'plugins.read', async () => ({
    status: 'ok',
    version: api.plugin.version,
  }))

  api.cms.hooks.on('publish.after', async (event) => {
    console.log('Publish finished – plugin can react')
  })
}

Successful activation sets lifecycleStatus to active, making the plugin's server-side API reachable and its functionality live.

Deactivate Phase

Deactivation gracefully shuts down plugin functionality without removing the package. The host calls deactivate(api) if exported, then disposes the sandbox VMs, unregisters module packs, and removes runtime registrations.

export function deactivate(api) {
  // Clean up temporary resources (e.g., cancel timers)
  api.cms.schedule.cancel('acme.workflow.cleanup')
}

The status transitions to disabled, meaning the plugin remains installed on disk but its functionality is turned off.

Migrate Phase

Migration executes exclusively during version upgrades when the new package declares a migrate hook. The host passes a context object containing fromVersion, allowing the plugin to transform persisted data or schema.

export function migrate(ctx, api) {
  // Migrate stored settings from 1.0.0 → 2.0.0
  if (ctx.fromVersion === '1.0.0') {
    const old = await api.cms.settings.get('apiKey')
    await api.cms.settings.replace({ newApiKey: old })
  }
}

Unlike other phases, migration does not change the lifecycleStatus; the plugin remains active after successful data transformation.

Uninstall Phase

Uninstallation performs complete removal. If the plugin is currently active, the host first runs the deactivate phase. It then calls uninstall(api), removes database rows, scheduled tasks, secrets, and deletes the upload directory.

export function uninstall(api) {
  // Delete the plugin's collection
  const coll = api.cms.storage.collection('approvals')
  await coll.deleteAll()
}

After completion, the entry is removed from the installed_plugins table entirely.

Error Handling and Recovery Mechanisms

If any lifecycle hook throws an exception, the host rolls back to the previous lifecycle state and records the error details in installed_plugins.lastError, setting lifecycleStatus to error. This guarantees atomic transitions—either the plugin reaches the new state or reverts to the last known good configuration.

Administrators can force-remove a failed plugin via DELETE /admin/api/cms/plugins/:id?force=true, implemented in server/handlers/cms/plugins/lifecycle.ts. This operation skips all hooks and tears down the worker, schedule rows, assets, and database entries immediately.

For runtime crashes, the host logs the event with the [plugin:<id>] prefix, creates a row in plugin_crash_events, and attempts auto-respawn. After three crashes within five minutes, the plugin enters the error state and requires manual restart.

All lifecycle events broadcast over Server-Sent Events (SSE) according to the PluginEventSchema defined in src/core/plugins/events.ts, enabling the admin UI to display real-time toasts and status badges.

Key Implementation Files

Understanding the Instatic plugin lifecycle requires familiarity with these core modules:

Summary

  • The Instatic plugin lifecycle consists of five phases: install, activate, deactivate, migrate, and uninstall, each with optional exported hooks.
  • The host manages sandboxed execution using Bun Workers and QuickJS-WASM VMs, persisting status in the installed_plugins table.
  • Atomic transitions ensure rollback to the previous state if any hook throws, with errors recorded in lastError.
  • Migration only fires during version upgrades and receives fromVersion context for data transformation.
  • Force removal bypasses hooks entirely via the force=true query parameter for corrupted or stuck plugins.
  • All state changes broadcast via SSE using schemas defined in src/core/plugins/events.ts.

Frequently Asked Questions

What happens if a plugin's activate hook throws an error?

If the activate hook throws, the host rolls back the transition and sets lifecycleStatus to error while recording the exception details in installed_plugins.lastError. The plugin remains in its previous state (typically installed) and does not become active, preventing broken code from affecting the system.

Can I skip the deactivate hook when uninstalling a plugin?

No, the host automatically calls deactivate if the plugin is currently active before executing uninstall. However, administrators can use force removal via DELETE /admin/api/cms/plugins/:id?force=true to skip all hooks entirely, which immediately tears down the worker and deletes all data without graceful cleanup.

When does the migrate hook execute in the Instatic plugin lifecycle?

The migrate hook executes only during version upgrades when the newly uploaded package exports this function. The host passes a context object containing fromVersion, allowing the plugin to transform persisted data. Unlike other phases, successful migration does not change the lifecycleStatus; the plugin remains active throughout the process.

How does Instatic handle plugin crashes during the active phase?

When a worker crashes, the host logs the event with the [plugin:<id>] identifier, creates a crash event record, and attempts auto-respawn. If the plugin crashes three times within five minutes, the host parks it in the error state, requiring manual intervention through the admin UI or API to restart.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →