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

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. The archive must contain a plugin.json manifest—parsed by parsePluginManifest in 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 Bun Worker → QuickJS VM IIFE assigning globalThis.__plugin_exports
modules/index.js Canvas module pack (server QuickJS or browser) ESM default export
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 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, which instantiates a QuickJS VM from 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—and compiled into a single string artifact at 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:

  • 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

// 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 handles request routing and permission enforcement.

Example: Scheduled Job Registration

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.

Example: Editor Command Registration

// 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.

Plugin Lifecycle and State Management

Plugins export lifecycle hooks invoked by the host runtime in 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 emits PluginEvent objects defined in src/core/plugins/events.ts.

The admin UI consumes this stream through src/admin/pages/plugins/utils/pluginEventStream.ts, with React components subscribing via usePluginEventBridge for live state synchronization.

Example: Custom Event Emission

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

Permission Enforcement Architecture

Permissions declared in plugin.json undergo validation at three distinct layers:

  1. Build-time lintinstatic-plugin lint scans bundles for forbidden literals (e.g., import 'node:fs', Bun.spawn)
  2. Install-time validationassertSandboxSafe re-scans the uploaded zip before activation
  3. Runtime verificationapi methods check grantedPermissions before executing privileged operations

Forbidden APIs are cataloged in the documentation and continuously validated by plugin-sandbox-invariants.test.ts.

Module Packs and Frontend Assets

Canvas module packs load into a dedicated QuickJS VM via 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. 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. 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, validated by src/core/plugins/manifest.ts
  • Sandboxed server execution in QuickJS-WASM prevents system access, with strict resource limits in 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 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 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.

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 →