Instatic Plugin Manifest Structure and Lifecycle Hooks: The Complete Guide

Every Instatic plugin is a self-describing zip archive containing a TypeBox-validated plugin.json manifest and optional entry-point bundles, exposing five server-side lifecycle hooks—install, activate, deactivate, uninstall, and migrate—that the host orchestrates during installation, activation, and version upgrades.

Instatic treats plugins as strictly validated, self-contained extensions that interact with the core CMS through a declarative manifest system. The platform enforces this contract via a rigorous TypeBox schema located in src/core/plugins/manifest.ts, ensuring every plugin declares its permissions, resources, and entry points before execution. Developers implement deterministic lifecycle hooks to safely initialize database tables, register routes, and handle version migrations without risking data integrity.

Plugin Manifest Structure

The plugin.json manifest serves as the single source of truth for plugin identity, capabilities, and asset locations. Instatic validates this file against manifestSchema in src/core/plugins/manifest.ts using the parsePluginManifest function (lines 85‑96), which throws descriptive errors for any schema violations.

Required Core Fields

Every manifest must define these fields with strict pattern constraints:

  • id: A kebab-case identifier following the pattern ^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$ (e.g., vendor.name). This dotted namespace ensures global uniqueness across the marketplace.
  • name: Human-readable title (1‑80 characters).
  • version: SemVer string matching ^\d+\.\d+\.\d+…$.
  • apiVersion: Integer ≥ 1 indicating the host API compatibility target. The validator explicitly checks compatibility via isCompatiblePluginApiVersion.
  • permissions: Array of values from PLUGIN_PERMISSION_VALUES declaring required host capabilities (e.g., cms.storage, frontend.assets).

Optional Metadata and Security

The schema includes optional fields for marketplace presentation and security hardening:

  • description: ≤ 500 characters.
  • author, license, homepage, repository: Validated objects with URL/email constraints.
  • grantedPermissions: Populated post-install by the host to record actually assigned rights.
  • networkAllowedHosts: Whitelist patterns (validated against NETWORK_HOST_PATTERN) for outbound HTTP when network.outbound permission is requested.
  • contentAccess: Fine-grained CRUD rights on CMS tables as an array of { table: slug, modes: [...] } objects.

Resources, Assets, and Admin Extensions

Advanced functionality requires declaring structural extensions:

  • resources: Array defining tables created via cms.storage, including field definitions (text, number, toggle, etc.).
  • entrypoints: Object mapping bundle types (server, editor, admin, module) to safe asset paths validated against SAFE_ASSET_PATH_PATTERN.
  • frontend.assets: Declarative tags (script, style, meta) injected into published pages when the frontend.assets permission is granted. Each tag specifies src, placement (e.g., head-end), and strategy (defer, async).
  • assetBasePath: Safe path prefix /uploads/plugins/{id}/{version} for plugin-specific uploads.
  • adminPages: Array of UI extensions (markdown, map, resource, app) rendered inside the admin console.
  • pack: Reference to a visual-component zip bundle via { path: safeAssetPath }.
  • settings: Declarative configuration UI rendered in the admin panel (text, textarea, number, toggle, select).

Manifest Validation

Validation occurs at runtime through parsePluginManifest in src/core/plugins/manifest.ts, which applies strict regex patterns including MANIFEST_SLUG_PATTERN and SAFE_ASSET_PATH_PATTERN. The function explicitly guards against API version mismatches:

export function parsePluginManifest(input: unknown): PluginManifest {
  // … schema parsing …
  if (!isCompatiblePluginApiVersion(data.apiVersion)) {
    throw new Error(`Plugin manifest targets unsupported API version ${data.apiVersion}`);
  }
  return data;
}

If any field violates its constraints—such as an invalid kebab-case id or an unsafe asset path—the validator throws before the plugin enters the lifecycle hook phase.

Lifecycle Hooks

Instatic plugins interact with the host through explicitly defined lifecycle hooks declared in src/core/plugin-sdk/types/lifecycle.ts. The host loads these hooks from the server entrypoint and registers them on an internal hook bus (src/core/plugins/hookBus.ts).

Available Server Hooks

Plugins may export any combination of these five async functions:

  • install: Invoked once during initial installation to allocate resources.
  • activate: Called when enabling the plugin or completing an upgrade; ideal for registering routes and caches.
  • deactivate: Executed when disabling the plugin or before an upgrade; use for temporary cache cleanup.
  • uninstall: Final cleanup hook for dropping tables and removing persisted data.
  • migrate: Receives { fromVersion } during upgrades to perform data transformations.

Upgrade Orchestration

During version upgrades, the host guarantees atomicity by following a strict sequence defined in src/core/plugin-sdk/types/lifecycle.ts (lines 27‑33):

  1. deactivate the old version (if running).
  2. Unpack new assets replacing the previous bundle.
  3. migrate the new version, passing the previous version string to guide data transformations.
  4. activate the new version.

If migrate or activate throws, the host automatically rolls back to the previous version and re-activates it, ensuring the platform remains in a consistent state.

Implementation Examples

Minimal Manifest

A valid plugin.json declaring a CMS resource and frontend asset:

{
  "id": "acme.widget",
  "name": "Acme Widget",
  "version": "1.2.0",
  "apiVersion": 3,
  "permissions": ["cms.storage", "frontend.assets"],
  "resources": [
    {
      "id": "widget",
      "title": "Widget",
      "fields": [
        { "id": "title", "label": "Title", "type": "text", "required": true },
        { "id": "price", "label": "Price", "type": "number" }
      ]
    }
  ],
  "adminPages": [
    {
      "id": "overview",
      "title": "Widget Overview",
      "content": {
        "kind": "markdown",
        "body": "# Welcome to the Widget plugin"

      }
    }
  ],
  "frontend": {
    "assets": [
      {
        "kind": "script",
        "src": "/uploads/plugins/acme.widget/1.2.0/tracker.js",
        "placement": "head-end",
        "strategy": "defer"
      }
    ]
  }
}

TypeScript SDK Integration

The definePlugin builder in src/core/plugin-sdk/builders/definePlugin.ts streamlines manifest creation while preserving full type safety:

import { definePlugin, permissions } from '@instatic/plugin-sdk';
import callout from './modules/callout';
import pack from './pack';

export default definePlugin({
  id: 'acme.ui-kit',
  name: 'Acme UI Kit',
  version: '1.0.0',
  description: 'Cards, tiers, testimonials, and a class pack.',
  permissions: [permissions.modulesRegister, permissions.visualComponentsRegister],
  modules: [callout],
  pack,
  editor: () => import('./editor'),
  server: () => import('./server'),
  frontend: () => import('./frontend/tracker'),
});

Server-Side Lifecycle Implementation

Implement hooks in your server entrypoint (src/plugins/acme.widget/server.ts) to manage resources and routing:

export const install = async ({ api }) => {
  console.log('Installing Acme Widget – allocating DB tables');
  await api.cms.storage.createResource('widget');
};

export const migrate = async ({ fromVersion }, { api }) => {
  console.log(`Migrating from ${fromVersion} to 1.2.0`);
  if (fromVersion < '1.2.0') {
    await api.cms.storage.addField('widget', { id: 'description', type: 'longtext' });
  }
};

export const activate = async ({ api }) => {
  console.log('Activating Acme Widget – registering routes');
  api.cms.routes.register('GET', '/widget/:id', async (req) => {
    const widget = await api.cms.storage.get('widget', req.params.id);
    return { json: widget };
  });
};

export const deactivate = async ({ api }) => {
  console.log('Deactivating – cleaning up temporary caches');
};

export const uninstall = async ({ api }) => {
  console.log('Uninstall – dropping resource tables');
  await api.cms.storage.deleteResource('widget');
};

Summary

  • Manifest Location: Every plugin requires a root-level plugin.json validated by parsePluginManifest in src/core/plugins/manifest.ts against a strict TypeBox schema.
  • Critical Fields: id (kebab-case dotted), version (SemVer), apiVersion (integer), and permissions array are mandatory; optional fields cover resources, adminPages, frontend.assets, and settings.
  • Validation Patterns: The host enforces MANIFEST_SLUG_PATTERN, SAFE_ASSET_PATH_PATTERN, and NETWORK_HOST_PATTERN to prevent unsafe identifiers and paths.
  • Five Lifecycle Hooks: install, activate, deactivate, uninstall, and migrate defined in src/core/plugin-sdk/types/lifecycle.ts.
  • Atomic Upgrades: The host executes deactivate → unpack → migrate → activate, rolling back automatically if migration fails.
  • Hook Registration: Server hooks are loaded from the manifest's server entrypoint and registered on the internal hook bus at src/core/plugins/hookBus.ts.

Frequently Asked Questions

What file defines the Instatic plugin manifest structure?

The TypeBox schema defining all manifest fields resides in src/core/plugins/manifest.ts. This file exports manifestSchema and the parsePluginManifest function that validates plugin.json against regex patterns like MANIFEST_SLUG_PATTERN and SAFE_ASSET_PATH_PATTERN.

Which lifecycle hooks are available for Instatic plugins?

Plugins can implement five server-side hooks defined in src/core/plugin-sdk/types/lifecycle.ts: install, activate, deactivate, uninstall, and migrate. These hooks allow plugins to initialize resources, register routes, handle version migrations, and perform cleanup.

How does Instatic handle plugin upgrades and migrations?

During an upgrade, the host first calls deactivate on the old version, unpacks new assets, then runs migrate on the new version with a fromVersion parameter, and finally calls activate. If either migrate or activate throws, the host rolls back to the previous version to maintain system consistency.

What validation schema does Instatic use for plugin manifests?

Instatic uses TypeBox via the manifestSchema object in src/core/plugins/manifest.ts to enforce strict types, string patterns (kebab-case IDs, SemVer versions), and safe asset paths. The parsePluginManifest function throws explicit validation errors before the plugin reaches the lifecycle hook stage.

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 →