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 viaisCompatiblePluginApiVersion.permissions: Array of values fromPLUGIN_PERMISSION_VALUESdeclaring 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 againstNETWORK_HOST_PATTERN) for outbound HTTP whennetwork.outboundpermission 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 viacms.storage, including field definitions (text, number, toggle, etc.).entrypoints: Object mapping bundle types (server,editor,admin,module) to safe asset paths validated againstSAFE_ASSET_PATH_PATTERN.frontend.assets: Declarative tags (script, style, meta) injected into published pages when thefrontend.assetspermission is granted. Each tag specifiessrc,placement(e.g.,head-end), andstrategy(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):
deactivatethe old version (if running).- Unpack new assets replacing the previous bundle.
migratethe new version, passing the previous version string to guide data transformations.activatethe 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.jsonvalidated byparsePluginManifestinsrc/core/plugins/manifest.tsagainst a strict TypeBox schema. - Critical Fields:
id(kebab-case dotted),version(SemVer),apiVersion(integer), andpermissionsarray are mandatory; optional fields coverresources,adminPages,frontend.assets, andsettings. - Validation Patterns: The host enforces
MANIFEST_SLUG_PATTERN,SAFE_ASSET_PATH_PATTERN, andNETWORK_HOST_PATTERNto prevent unsafe identifiers and paths. - Five Lifecycle Hooks:
install,activate,deactivate,uninstall, andmigratedefined insrc/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
serverentrypoint and registered on the internal hook bus atsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →