Instatic Plugin Manifest Schema and Lifecycle Hooks: Developer Reference
The Instatic plugin manifest schema is a strict TypeBox-validated configuration defined in 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 against a strict TypeBox schema (manifestSchema) located in 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 callsisCompatiblePluginApiVersionand 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 ofPLUGIN_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 whennetwork.outboundpermission 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 thefrontend.assetspermission.resources: Array of table definitions (id, fields) created viacms.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:
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):
deactivatethe currently running version.- Unpack new assets from the updated zip.
migratethe new version, passing the previous version string so the plugin can transform legacy data.activatethe 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). 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). This bus dispatches events to the correct plugin context while enforcing permission boundaries.
Implementation Examples
Minimal Plugin Manifest
The following plugin.json declares a widget plugin with storage permissions, a CMS resource, and frontend asset injection:
{
"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 to construct manifests programmatically:
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:
// 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.jsonschema via TypeBox insrc/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, andmigratefunctions 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) 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 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. This bus manages execution timing and provides the api context to each hook function.
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 →