# How to Create and Publish a Custom Instatic Plugin: Complete Developer Guide

> Learn how to create and publish a custom Instatic plugin with this complete developer guide. Build powerful extensions for your Instatic platform today.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-08-02

---

**Instatic plugins are distributed as zip packages containing a [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/capabilities.md).

### Plugin Manifest Validation

Before activation, Instatic validates the [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) manifest using the `parsePluginManifest` function in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```json
{
  "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`](https://github.com/CoreBunch/Instatic/blob/main/server/index.ts) and export an `activate` function. This function receives the SDK API and is invoked by [`server/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts) during server startup or when explicitly enabled by an administrator.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) match the internal zip structure exactly, as [`server/plugins/package.ts`](https://github.com/CoreBunch/Instatic/blob/main/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:

```bash
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts) to register routes and handlers
- **Manifest validation** occurs via `parsePluginManifest` in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts), enforcing strict schema requirements
- **Installation** extracts files to `uploads/plugins/<id>/<version>/` using [`server/plugins/package.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) if the [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts).