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:
docs/features/plugin-system.md– The authoritative documentation for the lifecycle diagram and manifest specifications.server/plugins/runtime.ts– ContainshandleServerPluginRuntimeRequest, the runtime entry point that dispatches lifecycle hooks.server/plugins/package.ts– HousesassertSandboxSafe, which validates uploaded zips and enforces security constraints.server/handlers/cms/plugins/lifecycle.ts– HTTP endpoint handler triggering install, activate, deactivate, migrate, and uninstall operations.src/core/plugins/manifest.ts– Parses and validatesplugin.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_pluginstable. - 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
fromVersioncontext for data transformation. - Force removal bypasses hooks entirely via the
force=truequery 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →