# Instatic Plugin System Architecture: Complete Technical Guide to Manifests, SDK APIs, and Sandboxed Execution

> Explore Instatic's plugin system architecture. Learn about manifests, SDK APIs, and sandboxed QuickJS-WASM execution for secure plugin development.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: architecture
- Published: 2026-08-01

---

**Instatic's plugin system uses a manifest-driven package format with sandboxed server-side execution in QuickJS-WASM and unsandboxed admin/editor runtimes, mediated by typed SDK APIs with granular permission enforcement.**

The **Instatic plugin system architecture** separates concerns across four distinct layers: package structure, sandboxed server runtime, unsandboxed admin runtime, and typed SDK APIs. This design enables third-party code to extend the CMS safely while preventing unauthorized access to system resources. The source code in `CoreBunch/Instatic` implements these boundaries through strict manifest validation, WebAssembly-based isolation, and runtime permission checks.

## Package Structure and Manifest Contract

Every Instatic plugin is a zip archive generated from [`instatic-plugin.config.ts`](https://github.com/CoreBunch/Instatic/blob/main/instatic-plugin.config.ts). The archive must contain a [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) manifest—parsed by `parsePluginManifest` in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts)—that serves as the source of truth for installation, permissions, and bundled entrypoints.

### Entrypoint Matrix

Plugins ship multiple entrypoints targeting different execution contexts:

| Entrypoint | Runtime Environment | Bundle Format |
|------------|---------------------|---------------|
| [`server/index.js`](https://github.com/CoreBunch/Instatic/blob/main/server/index.js) | Bun Worker → QuickJS VM | IIFE assigning `globalThis.__plugin_exports` |
| [`modules/index.js`](https://github.com/CoreBunch/Instatic/blob/main/modules/index.js) | Canvas module pack (server QuickJS or browser) | ESM default export |
| [`editor/index.js`](https://github.com/CoreBunch/Instatic/blob/main/editor/index.js) | Unsandboxed admin window | ESM |
| `admin/*.js` | Unsandboxed admin app page | ESM |
| `frontend/*.js` | Published-page assets | ESM |

The manifest schema in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) validates all fields at build and install time, ensuring structural integrity before any code executes.

## Sandboxed Server Runtime (QuickJS-WASM)

The **server-side sandbox** provides the strongest isolation guarantees. Each installed plugin receives its own **Bun Worker** defined in [`server/plugins/pluginWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/pluginWorker.ts), which instantiates a **QuickJS VM** from [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts).

### VM Boundary and Bootstrap

The VM executes with zero access to Node.js or Bun APIs, no filesystem privileges, and no environment variable access. Network capabilities require explicit `network.outbound` permission paired with an allowlist (`networkAllowedHosts`).

The runtime bootstrap is authored in TypeScript under `server/plugins/quickjs/bootstrap/src/`—notably [`pluginRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/pluginRuntime.ts)—and compiled into a single string artifact at [`server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts). This **single-source design** ensures the VM boundary remains consistent while enabling rapid iteration via `bun run bootstrap:sync`.

### Resource Limits

Strict budgets prevent resource exhaustion, configured in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts):

- **Heap:** 64 MiB
- **Stack:** 1 MiB
- **Eval timeout:** 5 seconds
- **Scheduled job timeout:** 5 minutes
- **RPC timeout:** 30 seconds

The host enforces these limits through `requestFromWorker` in the worker layer.

## Unsandboxed Admin and Editor Runtimes

Entrypoints marked `editor` or admin-app pages (`adminPages[].content.kind === "app"`) execute **unsandboxed** in the main admin window. These contexts have full browser privileges—React components, DOM manipulation, and unrestricted imports—gated by the `editor.code` permission.

Unlike the server sandbox, these modules import the browser-side SDK directly from `src/core/plugin-sdk/` and operate without VM overhead.

## Typed SDK and API Surface

All plugin code receives a unified `api` object exposing **namespaced sub-APIs**. Every namespace routes through TypeBox schemas for compile-time safety and runtime validation.

| Namespace | Capability | Required Permission |
|-----------|------------|-------------------|
| `api.plugin.*` | Metadata, logging, asset URLs | — |
| `api.cms.routes.*` | HTTP route registration (`GET`, `POST`, …) | `cms.routes` (`cms.routes.public` for anonymous) |
| `api.cms.storage.*` | Per-plugin collection CRUD | `cms.storage` |
| `api.cms.hooks.*` | Event registration and emission | `cms.hooks` |
| `api.cms.loops.*` | Dynamic content loop sources | `loops.register` |
| `api.cms.content.*` | CMS table CRUD and tree mutation | `cms.content.*` (read/write/publish/delete) |
| `api.cms.schedule.*` | Periodic job registration | `cms.schedule` |
| `api.editor.*` | Commands, toolbar buttons, palettes, panels, canvas overlays | Various `editor.*` permissions |
| `api.dashboard.widgets.*` | Dashboard widget registration | `dashboard.widgets.register` |
| `api.cms.settings.*` | Plugin settings with encrypted secrets | `cms.settings` |

### Example: Server-Side Route Registration

```typescript
// server/index.js (sandboxed)
export async function activate(api) {
  // Only available if manifest granted `cms.routes`
  api.cms.routes.get('/status', 'plugins.read', async ({ req }) => {
    return { ok: true, plugin: api.plugin.id };
  });
}

```

The route dispatcher in [`server/plugins/host/routeIo.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/host/routeIo.ts) handles request routing and permission enforcement.

### Example: Scheduled Job Registration

```typescript
export function activate(api) {
  api.cms.schedule.daily('cleanup', '03:00', async () => {
    const items = await api.cms.storage.collection('temp').list();
    await Promise.all(
      items.records.map(r => api.cms.storage.collection('temp').delete(r.id))
    );
  });
}

```

Implementation resides in [`server/plugins/scheduler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/scheduler.ts).

### Example: Editor Command Registration

```tsx
// editor/index.js (unsandboxed)
export function activate(api) {
  api.editor.commands.register({
    id: 'acme.workflow.approve',
    label: 'Approve current item',
    iconName: 'check',
    run: async () => {
      await api.cms.storage.collection('approvals').create({
        title: 'New',
        approved: true
      });
    },
  });
}

```

Editor SDK types are defined in [`src/core/plugin-sdk/types/editorApi.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/editorApi.ts).

## Plugin Lifecycle and State Management

Plugins export lifecycle hooks invoked by the host runtime in [`server/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts):

- `install` — Initial setup
- `activate` — Runtime initialization
- `deactivate` — Graceful shutdown
- `uninstall` — Cleanup and removal
- `migrate` — Schema or data migrations

Errors in any hook trigger automatic rollback and transition the plugin to the `error` state, with details recorded in `lastError`. The full lifecycle flow is documented in the plugin system documentation and enforced by the runtime dispatcher.

## Event Broadcasting and Real-Time Updates

Lifecycle transitions, crashes, and recoveries propagate via **Server-Sent Events**. The broadcaster in [`server/plugins/eventBroadcaster.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/eventBroadcaster.ts) emits `PluginEvent` objects defined in [`src/core/plugins/events.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/events.ts).

The admin UI consumes this stream through [`src/admin/pages/plugins/utils/pluginEventStream.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/plugins/utils/pluginEventStream.ts), with React components subscribing via `usePluginEventBridge` for live state synchronization.

### Example: Custom Event Emission

```typescript
export async function activate(api) {
  api.cms.hooks.emit('my.custom.event', { foo: 'bar' });
}

```

## Permission Enforcement Architecture

Permissions declared in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) undergo validation at three distinct layers:

1. **Build-time lint** — `instatic-plugin lint` scans bundles for forbidden literals (e.g., `import 'node:fs'`, `Bun.spawn`)
2. **Install-time validation** — `assertSandboxSafe` re-scans the uploaded zip before activation
3. **Runtime verification** — `api` methods check `grantedPermissions` before executing privileged operations

Forbidden APIs are cataloged in the documentation and continuously validated by [`plugin-sandbox-invariants.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/plugin-sandbox-invariants.test.ts).

## Module Packs and Frontend Assets

**Canvas module packs** load into a dedicated QuickJS VM via [`server/plugins/modulePackVm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/modulePackVm.ts), enabling server-side rendering of dynamic content.

**Frontend assets** declared in `plugin.json.frontend.assets` are injected into every published page by [`server/publish/frontendInjections.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/frontendInjections.ts). Both inclusion and execution require the `frontend.assets` permission.

## Secrets Management and Encryption

Secret settings (`type: "password"` with `secret: true`) are encrypted at rest in the `plugin_secrets` table, managed by [`server/repositories/pluginSecrets.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/pluginSecrets.ts). These values never leave the server process; browser-side code receives only masked placeholders (`***`). The `plugin-secrets-never-leak` architecture test enforces this invariant.

## Summary

- **Manifest-driven packages** define entrypoints, permissions, and metadata through [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json), validated by [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts)
- **Sandboxed server execution** in QuickJS-WASM prevents system access, with strict resource limits in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts)
- **Unsandboxed admin/editor code** runs with full browser privileges when `editor.code` permission is granted
- **Typed SDK APIs** (`api.*`) provide namespaced capabilities with compile-time and runtime validation
- **Three-layer permission enforcement** spans build-time linting, install-time scanning, and runtime checks
- **Lifecycle hooks** (`install`, `activate`, `deactivate`, `uninstall`, `migrate`) with automatic rollback on failure
- **Real-time event streaming** via SSE notifies the admin UI of state changes
- **Encrypted secrets** never transit to browser contexts, backed by architectural tests

## Frequently Asked Questions

### What sandbox technology does Instatic use for server-side plugins?

Instatic uses **QuickJS compiled to WebAssembly** for server-side sandboxing. Each plugin runs in a dedicated Bun Worker that instantiates a QuickJS VM with no access to Node.js or Bun APIs. The bootstrap code is authored in TypeScript and compiled to a single string artifact, ensuring consistent VM boundaries while maintaining build flexibility.

### How does Instatic prevent plugins from accessing unauthorized system resources?

Protection operates at **three layers**: build-time linting (`instatic-plugin lint`) blocks forbidden imports; install-time validation (`assertSandboxSafe`) re-scans bundles; and runtime checks verify `grantedPermissions` before executing any privileged SDK method. Additionally, the QuickJS VM has zero filesystem, environment, or network access without explicit manifest declarations and allowlists.

### Can plugins run code in the browser with full DOM access?

Yes, but only through specific entrypoints. Code in [`editor/index.js`](https://github.com/CoreBunch/Instatic/blob/main/editor/index.js) and admin-app pages runs **unsandboxed** with full browser privileges, including React and DOM access. This requires the `editor.code` permission. All other plugin code—`server`, `modules`, and `frontend` entrypoints—either runs in QuickJS-WASM or as isolated scripts without direct DOM manipulation capabilities.

### What happens when a plugin lifecycle hook fails?

The runtime in [`server/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts) automatically **rolls back** the operation and transitions the plugin to the `error` state. The failure details are recorded in `lastError`, and an event is broadcast via Server-Sent Events to notify the admin UI. The plugin remains in this state until manually reactivated or uninstalled.