How to Create and Publish a Custom Instatic Plugin: Complete Developer Guide
Instatic plugins are distributed as zip packages containing a plugin.json manifest and TypeScript entry points that run inside a QuickJS-WASM sandbox, activated via the activate(api) lifecycle hook in server/plugins/runtime.ts.
Instatic treats plugins as first-class extensions that extend CMS functionality while maintaining security through capability-based permissions. Whether you are building a custom content transformation tool or an admin dashboard widget, understanding the plugin architecture in the CoreBunch/Instatic repository is essential for successful development.
Understanding the Instatic Plugin Architecture
Instatic's plugin system is designed around isolation and explicit permissions. When the server starts, it scans uploads/plugins/ for installed packages, validates manifests using src/core/plugins/manifest.ts, and boots each plugin in a sandboxed environment.
The QuickJS-WASM Sandbox
Every plugin executes within a QuickJS-WASM sandbox, ensuring that third-party code cannot compromise the host system. The plugin SDK located in src/core/plugin-sdk/ mediates all communication between the plugin and the Instatic core. When a plugin calls methods to read content or register routes, these requests are validated against the capability system—specifically capabilities like plugins.install and plugins.lifecycle as defined in docs/reference/capabilities.md.
Plugin Manifest Validation
Before activation, Instatic validates the plugin.json manifest using the parsePluginManifest function in src/core/plugins/manifest.ts. This schema enforcement ensures that every plugin declares its ID, version, entry points, and required permissions upfront.
Step-by-Step: Create a Custom Instatic Plugin
Step 1: Scaffold Your Plugin Project
Start by copying the boilerplate from examples/plugins/template/ in the CoreBunch/Instatic repository. This template provides the standard directory structure and TypeScript configuration required for both server-side and client-side code.
Step 2: Define the plugin.json Manifest
Create a plugin.json file at the project root. This manifest describes your plugin's identity, entry points, and security requirements. The manifest is validated by parsePluginManifest against strict TypeBox patterns.
{
"id": "my.example",
"version": "1.0.0",
"name": "My Example Plugin",
"description": "Demo plugin that adds a hello-world page.",
"main": "server/index.ts",
"client": "client/index.ts",
"permissions": [
"cms.routes.public",
"cms.content.read",
"cms.content.write"
]
}
Key manifest fields:
id: Unique reverse-domain identifier for your pluginmain: Path to the server-side entry point (TypeScript)client: Path to the admin UI entry point (optional)permissions: Array of capabilities required from the host system
Step 3: Implement the Server Entry Point
Create server/index.ts and export an activate function. This function receives the SDK API and is invoked by server/plugins/runtime.ts during server startup or when explicitly enabled by an administrator.
import { registerRoute, registerRpc } from '@core/plugin-sdk';
export async function activate(api: any) {
// Register a public HTTP route
registerRoute('GET', '/hello', async () => ({
html: '<h1>Hello from My Example Plugin</h1>',
}));
// Register an RPC method callable from the client
registerRpc('my.example.getDate', async () => ({
date: new Date().toISOString(),
}));
}
The activate hook is the primary lifecycle event where you register HTTP routes, RPC handlers, scheduled jobs, and content transformations. All HTTP routes provided by the plugin are automatically mounted under /admin/api/cms/plugins/<id>/runtime/… via the handleRuntimeRoutes mechanism.
Step 4: Build the Client Bundle (Optional)
If your plugin requires admin UI components, implement client/index.ts. Use the SDK's UI primitives from @ui/components/* and wrap your components in plugin-specific error boundaries (plugin-page, plugin-editor-panel, or plugin-canvas-overlay) to prevent plugin crashes from destabilizing the admin shell.
import { useRpc } from '@core/plugin-sdk';
import { Button } from '@ui/components/Button';
export function HelloButton() {
const getDate = useRpc('my.example.getDate');
return (
<Button onClick={async () => {
const { date } = await getDate();
alert(`Server date: ${date}`);
}}>
Show Server Date
</Button>
);
}
Step 5: Package and Structure the Zip File
Bundle your plugin as a zip archive with the following structure:
my-example-plugin.zip
├─ plugin.json
├─ server/
│ └─ index.ts
└─ client/
└─ index.ts
Ensure that file paths in plugin.json match the internal zip structure exactly, as server/plugins/package.ts extracts these files to uploads/plugins/<id>/<version>/ during installation.
Installing and Activating Your Plugin
Install the plugin via the Instatic admin UI at /admin/plugins or using the REST API:
curl -X POST \
-H "Authorization: Bearer <admin-token>" \
-F "file=@my-example-plugin.zip" \
https://my-instatic.local/admin/api/cms/plugins/install
The server uses server/plugins/package.ts to unzip and validate the archive. Once installed, enable the plugin through the admin UI or SDK methods. Upon activation, Instatic loads your code into the QuickJS VM and executes the activate function, registering your routes and RPC handlers.
Publishing Your Instatic Plugin
When preparing to distribute your plugin to other Instatic installations:
- Version correctly: Update the
versionfield inplugin.jsonfollowing semantic versioning - Document capabilities: Clearly list required permissions so administrators understand the security implications (installing a plugin runs third-party code requiring the
plugins.installcapability) - Distribute the zip: Upload to a private npm registry, GitHub Releases, or custom HTTP server
- Catalog registration: If maintaining a public plugin catalog, reference your download URL in the catalog entry metadata
Summary
- Instatic plugins are zip packages containing
plugin.json, server code, and optional client code that run in a QuickJS-WASM sandbox - The
activate(api)function in your server entry point is called byserver/plugins/runtime.tsto register routes and handlers - Manifest validation occurs via
parsePluginManifestinsrc/core/plugins/manifest.ts, enforcing strict schema requirements - Installation extracts files to
uploads/plugins/<id>/<version>/usingserver/plugins/package.ts - All plugin-host communication flows through the SDK in
src/core/plugin-sdk/and is gated by the capability system - Use
examples/plugins/template/as your starting point for new plugin development
Frequently Asked Questions
What permissions do I need to install a custom plugin?
Installing a plugin requires the plugins.install capability, while managing plugin lifecycle states requires plugins.lifecycle. These capabilities are typically restricted to super-administrators because installing a plugin executes third-party code within the Instatic environment.
Can I use npm packages in my Instatic plugin?
Yes, but all dependencies must be bundled into your server and client entry points before packaging. The QuickJS-WASM sandbox does not provide Node.js built-in modules or access to the host file system, so ensure your build process creates self-contained JavaScript bundles from your TypeScript source.
How do I debug a plugin that fails to activate?
Check the server logs for validation errors from src/core/plugins/manifest.ts if the plugin.json is malformed. If the plugin loads but routes are unavailable, verify that your activate function correctly calls registerRoute and that the plugin has the necessary cms.routes.public capability in its permissions array.
Where are installed plugins stored on the file system?
Instatic extracts installed plugins to uploads/plugins/<id>/<version>/ according to the logic in server/plugins/package.ts. The server scans this directory at startup to discover available plugins, then activates them through the runtime system in server/plugins/runtime.ts.
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 →