# Instatic Tree Mutation API for Page and Visual Component Operations

> Explore the Instatic Tree mutation API for page and visual component operations. Learn how 11 pure mutation functions consistently manipulate NodeTree structures for your visual editor and plugin VM.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: api-reference
- Published: 2026-07-31

---

**The Instatic Tree mutation API exposes 11 pure mutation functions in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) that modify a `NodeTree<PageNode>` structure through the central `applyTreeOperation` dispatcher, enabling consistent page and visual component tree manipulations across the visual editor and plugin VM.**

Instatic represents every page and visual component (VC) hierarchy as a **`NodeTree<PageNode>`** living inside a **`SiteDocument`**. All structural changes—whether inserting nodes, moving subtrees, or updating props—flow through a strictly defined **Instatic Tree mutation API** centered in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts). These functions are pure (operating on Zustand Mutative drafts) and exposed via a single entry point used by both the admin interface and the plugin runtime.

## Core Data Structures

Understanding the mutation API requires familiarity with three foundational types defined across the core page-tree modules.

### NodeTree and PageNode

The **`NodeTree<PageNode>`** (defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts)) is the mutable container holding a `nodes` map, a `rootNodeId`, and document metadata. Each entry in the map is a **`PageNode`** ([`src/core/page-tree/pageNode.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/pageNode.ts)) representing a module instance with properties for module ID, props, children IDs, breakpoint overrides, class IDs, and bookkeeping fields (`parentId`, `label`, `locked`, `hidden`).

### TreeOperation Discriminated Union

All intended changes are described by a **`TreeOperation`** ([`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts)), a tagged union where each variant carries the minimal data required for a specific mutation. This strict typing ensures that the dispatcher can exhaustively handle every valid operation type without ambiguity.

## The 11 Core Mutation Functions

The Instatic Tree mutation API provides 11 named primitives for page/VC operations. Each function mutates the supplied draft tree directly and is implemented in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts).

- **`createNode(moduleId, defaults?)`** — Factory returning a fresh `PageNode` with a unique ID and optional default props.
- **`insertNode(tree, node, parentId, index?)`** — Adds a node as a child of the specified parent at the given index; omitting the index appends to the end.
- **`deleteNode(tree, nodeId)`** — Removes a node and **all its descendants** from the tree, cleaning up the parent’s children array.
- **`updateNodeProps(tree, nodeId, patch)`** — Performs a shallow merge of new props into the existing `node.props` object.
- **`setBreakpointOverride(tree, nodeId, breakpointId, patch)`** — Applies property overrides for a specific breakpoint.
- **`clearBreakpointOverride(tree, nodeId, breakpointId)`** — Removes breakpoint-specific overrides for the node.
- **`renameNode(tree, nodeId, label)`** — Updates the human-readable `node.label` field.
- **`toggleNodeLocked(tree, nodeId)`** — Flips the boolean `node.locked` flag.
- **`toggleNodeHidden(tree, nodeId)`** — Flips the boolean `node.hidden` flag.
- **`moveNode(tree, nodeId, newParentId, newIndex)`** — Re-parents a node or reorders it within the same parent.
- **`duplicateNode(tree, nodeId, options?)`** — Deep-clones a subtree, assigns fresh IDs to all cloned nodes, and inserts the clone immediately after the source node.
- **`wrapNode(tree, nodeId, containerModuleId, containerDefaults?)`** — Inserts a new container node and makes the target node its first child.
- **`pasteSubtree(tree, payload, parentId, index?, options?)`** — Inserts a foreign subtree (e.g., from clipboard) after regenerating all IDs to avoid collisions.

Additional bulk helpers such as `wrapNodes` and `moveNodes` build upon these primitives using the same reduction logic employed by the UI layer.

## The applyTreeOperation Dispatcher

All **Instatic Tree mutation API** calls converge on a single dispatcher: **`applyTreeOperation`** ([`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), lines 81–120). This function receives the tree draft and a `TreeOperation` object, switches on `op.kind`, and delegates to the appropriate mutation helper.

```typescript
// src/core/page-tree/mutations.ts
export function applyTreeOperation(
  tree: NodeTree<PageNode>,
  op: TreeOperation,
): ApplyTreeOperationResult {
  switch (op.kind) {
    case 'insertNode':
      insertNode(tree, op.node, op.parentId, op.index);
      return { tree, affectedNodeIds: [op.parentId, op.node.id] };
    case 'updateNodeProps':
      updateNodeProps(tree, op.nodeId, op.props);
      return { tree, affectedNodeIds: [op.nodeId] };
    case 'setBreakpointOverride':
      setBreakpointOverride(tree, op.nodeId, op.breakpoint, op.props);
      return { tree, affectedNodeIds: [op.nodeId] };
    case 'clearBreakpointOverride':
      clearBreakpointOverride(tree, op.nodeId, op.breakpoint);
      return { tree, affectedNodeIds: [op.nodeId] };
    case 'renameNode':
      renameNode(tree, op.nodeId, op.name);
      return { tree, affectedNodeIds: [op.nodeId] };
    case 'toggleNodeLocked':
      toggleNodeLocked(tree, op.nodeId);
      return { tree, affectedNodeIds: [op.nodeId] };
    case 'toggleNodeHidden':
      toggleNodeHidden(tree, op.nodeId);
      return { tree, affectedNodeIds: [op.nodeId] };
    case 'moveNode':
      const oldParent = getParent(tree, op.nodeId);
      moveNode(tree, op.nodeId, op.parentId, op.index);
      return { tree, affectedNodeIds: oldParent ? [op.nodeId, op.parentId, oldParent.id] : [op.nodeId, op.parentId] };
    case 'duplicateNode':
      const newId = duplicateNode(tree, op.nodeId);
      return { tree, affectedNodeIds: [op.nodeId, newId] };
    case 'wrapNode':
      const wrapperId = wrapNode(tree, op.nodeId, op.wrapper.moduleId, op.wrapper.defaults);
      return { tree, affectedNodeIds: [op.nodeId, wrapperId] };
    case 'deleteNode':
      const parent = getParent(tree, op.nodeId);
      deleteNode(tree, op.nodeId);
      return { tree, affectedNodeIds: parent ? [parent.id] : [] };
  }
}

```

The dispatcher never clones the tree; it mutates the supplied Zustand draft. Callers requiring an immutable snapshot must `structuredClone` the tree before invocation. Each case returns an `ApplyTreeOperationResult` containing the mutated tree and an array of affected node IDs for reactive UI updates.

## Practical Implementation Examples

The following examples demonstrate common **Instatic Tree mutation API** workflows using the raw helpers and the dispatcher:

```typescript
import {
  createNode,
  insertNode,
  moveNode,
  duplicateNode,
  wrapNode,
  applyTreeOperation,
} from '@core/page-tree';

// 1. Create a new button node and append it to the page root.
const btnNode = createNode('core/modules/button', { text: 'Click me' });
insertNode(pageTree, btnNode, pageTree.rootNodeId); // index omitted → append

// 2. Rename the button using the dispatcher.
applyTreeOperation(pageTree, {
  kind: 'renameNode',
  nodeId: btnNode.id,
  name: 'Primary button',
});

// 3. Duplicate the button (inserts clone after source automatically).
const cloneId = duplicateNode(pageTree, btnNode.id);

// 4. Move the clone into a column container at index 0.
applyTreeOperation(pageTree, {
  kind: 'moveNode',
  nodeId: cloneId,
  parentId: columnNodeId,
  index: 0,
});

// 5. Wrap the original button in a new flex container.
const wrapperId = wrapNode(
  pageTree,
  btnNode.id,
  'core/modules/flex',
  { direction: 'row' }
);

```

## Integration Points: Editor and Plugin VM

The **Instatic Tree mutation API** serves as the single source of truth for tree consistency across two distinct consumers:

- **Visual Editor**: The Zustand store ([`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts)) defines actions like `insertNode` and `moveNode` that wrap these helpers inside a `mutateActiveTree` call, ensuring UI state stays synchronized with the draft mutations.
- **Plugin VM**: Plugins executing in the QuickJS runtime invoke `applyTreeOperation` via the RPC endpoint `api.cms.content.tree(...).mutate` (see `src/core/plugins/quickjs/bootstrap/src/...`), allowing third-party code to perform structural changes safely.

Because both pathways funnel through the same `applyTreeOperation` dispatcher, **all page-level and VC-level structural changes obey identical validation and side-effect logic**.

## Summary

- Instatic represents pages and visual components as a **`NodeTree<PageNode>`** stored within a `SiteDocument`.
- The **Instatic Tree mutation API** consists of 11 pure functions located in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts).
- All mutations flow through the **`applyTreeOperation`** dispatcher, which interprets **`TreeOperation`** commands from [`operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/operationSchema.ts).
- Functions like **`insertNode`**, **`moveNode`**, **`duplicateNode`**, and **`wrapNode`** handle structural changes, while **`updateNodeProps`** and breakpoint helpers manage data.
- The API is consumed by both the **Zustand-based editor store** and the **plugin VM**, ensuring consistent behavior across the application.

## Frequently Asked Questions

### What is the difference between `insertNode` and `pasteSubtree`?

**`insertNode`** adds a single `PageNode` instance that you have already created (typically via `createNode`) into a specific parent at a specific index. **`pasteSubtree`** accepts a serialized foreign tree (e.g., from the clipboard), regenerates all node IDs to prevent collisions, and inserts the entire hierarchy. Use `insertNode` for programmatic creation and `pasteSubtree` for importing external or copied structures.

### How does the mutation API maintain immutability?

The functions themselves are pure but operate on **draft objects** supplied by Zustand’s Mutative middleware. The tree passed into `applyTreeOperation` is already a mutable draft; the dispatcher and helpers mutate it directly. If you need an immutable result, you must `structuredClone` the tree before calling the API, as the functions do not perform defensive copying.

### Can plugins directly import the mutation helpers?

No. Plugins run inside a QuickJS sandbox and do not have direct filesystem access to [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts). Instead, they invoke the **`api.cms.content.tree.mutate`** RPC method, which serializes a `TreeOperation` object and forwards it to the host environment where `applyTreeOperation` executes on the actual document state.

### What happens to child nodes when `deleteNode` is called?

The **`deleteNode`** mutation performs a recursive removal. When you delete a node, the function removes that node’s entry from the tree’s `nodes` map and filters its ID from its parent’s children array. Because the tree stores nodes in a flat map with references to children IDs, deleting a parent effectively orphans its entire subtree, and subsequent operations should treat those IDs as invalid unless the nodes were explicitly moved beforehand.