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

> Master the Instatic plugin lifecycle: install, activate, deactivate, migrate, and uninstall hooks. Understand how Instatic manages sandboxed execution and state persistence.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-08-01

---

**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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) manifest against the schema defined in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:

- **[`docs/features/plugin-system.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/features/plugin-system.md)** – The authoritative documentation for the lifecycle diagram and manifest specifications.
- **[`server/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts)** – Contains `handleServerPluginRuntimeRequest`, the runtime entry point that dispatches lifecycle hooks.
- **[`server/plugins/package.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/package.ts)** – Houses `assertSandboxSafe`, which validates uploaded zips and enforces security constraints.
- **[`server/handlers/cms/plugins/lifecycle.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/plugins/lifecycle.ts)** – HTTP endpoint handler triggering install, activate, deactivate, migrate, and uninstall operations.
- **[`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts)** – Parses and validates [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json), ensuring permission-manifest coherence before any lifecycle phase begins.

## 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`](https://github.com/CoreBunch/Instatic/blob/main/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.