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 plugin
  • main: 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:

  1. Version correctly: Update the version field in plugin.json following semantic versioning
  2. Document capabilities: Clearly list required permissions so administrators understand the security implications (installing a plugin runs third-party code requiring the plugins.install capability)
  3. Distribute the zip: Upload to a private npm registry, GitHub Releases, or custom HTTP server
  4. 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 by server/plugins/runtime.ts to register routes and handlers
  • Manifest validation occurs via parsePluginManifest in src/core/plugins/manifest.ts, enforcing strict schema requirements
  • Installation extracts files to uploads/plugins/<id>/<version>/ using server/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →