# Instatic Plugin Manifest Structure and Lifecycle Hooks: The Complete Guide

> Explore the Instatic plugin manifest structure and master its lifecycle hooks install activate deactivate uninstall and migrate for seamless plugin management and updates.

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

---

**Every Instatic plugin is a self-describing zip archive containing a TypeBox-validated [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```ts
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) declaring a CMS resource and frontend asset:

```json
{
  "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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/builders/definePlugin.ts) streamlines manifest creation while preserving full type safety:

```ts
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`](https://github.com/CoreBunch/Instatic/blob/main/src/plugins/acme.widget/server.ts)) to manage resources and routing:

```ts
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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) validated by `parsePluginManifest` in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts). This file exports `manifestSchema` and the `parsePluginManifest` function that validates [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.