# Instatic Plugin Manifest Schema and Lifecycle Hooks: Developer Reference

> Understand the Instatic plugin manifest schema and its lifecycle hooks. Securely manage metadata, permissions, resources, and entrypoints for your Instatic plugins.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: api-reference
- Published: 2026-07-26

---

**The Instatic plugin manifest schema is a strict TypeBox-validated configuration defined in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) that declares metadata, permissions, resources, and entrypoints, while lifecycle hooks (`install`, `activate`, `deactivate`, `uninstall`, `migrate`) are functions exported from server entrypoints that the host orchestrates through an internal hook bus.**

Every plugin in CoreBunch/Instatic is a self-describing zip archive centered around a validated manifest file. Understanding the Instatic plugin manifest schema and lifecycle hooks is essential for building secure, upgradable extensions that declare their capabilities upfront and respond gracefully to installation, activation, and migration events.

## Understanding the Plugin Manifest Schema

Instatic validates every [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) against a strict TypeBox schema (`manifestSchema`) located in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts). The `parsePluginManifest` function (lines 85–96) enforces type safety and throws descriptive errors if fields violate constraints like `MANIFEST_SLUG_PATTERN` or `SAFE_ASSET_PATH_PATTERN`.

### Required Identification Fields

Every manifest must declare four core identifiers:

- **`id`**: A kebab-case string following the dotted namespace pattern `^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$` (e.g., `acme.widget`). This becomes the unique plugin identifier.
- **`name`**: Human-readable title between 1 and 80 characters.
- **`version`**: SemVer format (`^\d+\.\d+\.\d+…$`) declaring the plugin release.
- **`apiVersion`**: Integer ≥ 1 specifying the host API version targeted. The validator calls `isCompatiblePluginApiVersion` and throws if the host cannot support the requested version.

### Security and Permission Declarations

The manifest explicitly declares what host capabilities a plugin requires:

- **`permissions`**: Array of `PLUGIN_PERMISSION_VALUES` (e.g., `cms.storage`, `frontend.assets`). The host refuses to load the plugin if these are not granted.
- **`grantedPermissions`**: Optional array populated by the host after installation, reflecting what was actually approved.
- **`networkAllowedHosts`**: Array of patterns (`^[a-z0-9]…$`) whitelisting outbound HTTP destinations when `network.outbound` permission is requested.
- **`contentAccess`**: Optional fine-grained CRUD rights on CMS tables, defined as an array of `{ table: slug, modes: [...] }` objects.

### Assets, Resources, and UI Extensions

The schema supports rich UI and data extensions:

- **`entrypoints`**: Object mapping contexts (`server`, `editor`, `admin`, `module`) to safe asset paths under `/uploads/plugins/...`.
- **`frontend.assets`**: Array of declarative tags (script, style, meta) injected into published pages; requires the `frontend.assets` permission.
- **`resources`**: Array of table definitions (id, fields) created via `cms.storage`.
- **`adminPages`**: Array of page definitions (markdown, map, resource, app) extending the admin console.
- **`settings`**: Array of configurable fields (text, textarea, number, toggle, select) rendered in the admin UI.
- **`assetBasePath`**: Optional safe path prefix (`/uploads/plugins/{id}/{version}`) for plugin static assets.
- **`pack`**: Optional path to a visual-component zip for class packs.

## Lifecycle Hooks Architecture

Lifecycle hooks allow plugins to execute logic during state transitions. The allowed hook names are defined as a union type in [`src/core/plugin-sdk/types/lifecycle.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/lifecycle.ts):

```ts
export type ServerPluginLifecycleHook =
  | 'install'
  | 'activate'
  | 'deactivate'
  | 'uninstall'
  | 'migrate';

```

### The Five Standard Hooks

Each hook receives a context object containing the `api` interface:

- **`install`**: Runs once when the plugin is first installed; ideal for creating database tables.
- **`activate`**: Runs when the plugin is enabled; register routes, hooks, and caches here.
- **`deactivate`**: Runs when the plugin is disabled or before an upgrade; clean up temporary state.
- **`uninstall`**: Runs when the plugin is removed; drop tables and delete persisted data.
- **`migrate`**: Runs during upgrades, receiving `{ fromVersion }` to adjust data schemas between versions.

### Upgrade Orchestration Sequence

When updating a plugin, the host orchestrates hooks in a specific sequence to ensure consistency (lines 27–33 of [`src/core/plugin-sdk/types/lifecycle.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/lifecycle.ts)):

1. **`deactivate`** the currently running version.
2. Unpack new assets from the updated zip.
3. **`migrate`** the new version, passing the previous version string so the plugin can transform legacy data.
4. **`activate`** the new version.

If `migrate` or `activate` throws an exception, the host automatically rolls back to the previous version and re-activates it, preventing partial upgrades.

### Hook Registration and Dispatch

Hooks are plain functions exported from the plugin’s server entrypoint module (e.g., [`src/plugins/my-plugin/server.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/plugins/my-plugin/server.ts)). The host loads the module via the `server` entrypoint declared in the manifest and registers the exported functions on the internal **hook bus** ([`src/core/plugins/hookBus.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/hookBus.ts)). This bus dispatches events to the correct plugin context while enforcing permission boundaries.

## Implementation Examples

### Minimal Plugin Manifest

The following [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) declares a widget plugin with storage permissions, a CMS resource, and frontend asset injection:

```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"
      }
    ]
  }
}

```

### Using the `definePlugin` Builder

Developers can use the TypeScript helper in [`src/core/plugin-sdk/builders/definePlugin.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/builders/definePlugin.ts) to construct manifests programmatically:

```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'),
});

```

### Implementing Server Lifecycle Hooks

Export named functions from your server entrypoint to handle lifecycle events:

```ts
// src/plugins/acme.widget/server.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

- **Strict Validation**: Instatic enforces the [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) schema via TypeBox in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts), ensuring all plugin IDs, paths, and permissions meet security patterns before installation.
- **Five Lifecycle Hooks**: Plugins export `install`, `activate`, `deactivate`, `uninstall`, and `migrate` functions from their server entrypoint to handle state transitions.
- **Atomic Upgrades**: The host runs `deactivate` → asset swap → `migrate` → `activate`, with automatic rollback if migration or activation fails.
- **Declarative Extensions**: The manifest defines CMS resources, admin pages, frontend assets, and configuration settings without imperative code.
- **Hook Bus Architecture**: The internal hook bus ([`src/core/plugins/hookBus.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/hookBus.ts)) registers and dispatches lifecycle events, isolating plugin execution contexts.

## Frequently Asked Questions

### What happens if a plugin declares an unsupported `apiVersion`?

The `parsePluginManifest` function in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) validates the field against `isCompatiblePluginApiVersion` and throws a clear error message: `Plugin manifest targets unsupported API version ${data.apiVersion}`. The host refuses to load the plugin until the developer updates the manifest to match a supported version.

### How does Instatic handle database schema changes during plugin updates?

Developers implement schema changes in the **`migrate`** hook, which receives the `{ fromVersion }` parameter. This allows conditional logic to add fields, transform data, or backfill records based on the previously installed version. If the migration throws, the host rolls back to the previous plugin version and re-activates it, preventing data corruption.

### Can a plugin inject arbitrary HTML into the frontend?

Yes, but only through the **`frontend.assets`** array in the manifest, which requires the `frontend.assets` permission. This array accepts declarative tag definitions (script, style, meta) with attributes like `placement` and `strategy`, rather than raw HTML strings, ensuring the host can sanitize URLs and enforce CSP policies.

### Where are lifecycle hooks registered at runtime?

When a plugin activates, the host loads the module specified by the **`server`** entrypoint in the manifest and registers any exported hook functions (`install`, `activate`, etc.) on the internal **hook bus** located at [`src/core/plugins/hookBus.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/hookBus.ts). This bus manages execution timing and provides the `api` context to each hook function.