# How Instatic's Page Tree Mutation System Works: Architecture and Implementation

> Discover how Instatic's page tree mutation system functions. Explore its architecture and implementation details with 11 pure mutation functions and a single entry point for editors and sandboxes.

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

---

**Instatic's page tree mutation system operates on a flat map of nodes (`NodeTree<PageNode>`) using 11 pure mutation functions centralized in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), all dispatched through a single `applyTreeOperation` entry point that serves both the Zustand-based visual editor and the QuickJS plugin sandbox.**

Instatic represents every page and visual component as a normalized tree structure rather than nested JSON objects. This architecture enables constant-time parent lookups and safe structural modifications while keeping the UI, server-side renderer, and untrusted plugin code perfectly synchronized through a unified mutation dispatcher.

## Flat Node Tree Architecture

At the core of Instatic's page tree mutation system lies a **flat node map** where relationships are maintained through string references rather than object nesting. The `NodeTree<PageNode>` structure (validated by [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts)) consists of:

- A `nodes` dictionary keyed by unique IDs generated via `nanoid()`
- A `rootNodeId` pointing to the tree's entry point
- Each `PageNode` containing `parentId`, `children` (an array of child IDs), `props`, and metadata flags

This design allows O(1) parent lookups via `getParent` in [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) and simplifies cycle detection when reorganizing nodes.

## The 11 Core Mutation Operations

All tree modifications flow through pure functions in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts). These operations are categorized by their structural impact:

### Node Creation and Insertion

**`createNode(moduleId, defaults?)`** ([mutations.ts#L48-L62](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L48-L62)) generates a fresh node with a unique `nanoid()` identifier and a `null` `parentId`.

**`insertNode(tree, node, parentId, index?)`** ([mutations.ts#L72-L92](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L72-L92)) adds the node to the flat `tree.nodes` map, stamps the `parentId`, and splices the node's ID into the parent's `children` array at the specified index.

### Property and Metadata Updates

Three functions handle reactive property changes without structural modification:
- **`updateNodeProps`** ([mutations.ts#L13-L38](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L13-L38)) merges changes into the shallow `props` map
- **`setBreakpointOverride`** and **`clearBreakpointOverride`** manage responsive design variants

UI-state mutations include **`renameNode`**, **`toggleNodeLocked`**, and **`toggleNodeHidden`** ([mutations.ts#L54-L70](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L54-L70)), which modify editor-specific flags without affecting the render output.

### Deletion and Duplication

**`deleteNode(tree, nodeId)`** ([mutations.ts#L98-L107](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L98-L107)) removes a node and all its descendants via `deleteSubtree`, automatically cleaning up the parent's `children` reference to prevent orphaned IDs.

**`duplicateNode`** ([mutations.ts#L33-L85](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L33-L85)) performs a deep clone of a subtree, generating fresh IDs with `nanoid()`, remapping class IDs to prevent style collisions, and inserting the clone immediately after the source node.

**`pasteSubtree`** ([mutations.ts#L90-L78](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L90-L78)) handles foreign tree insertion (e.g., from clipboard) by building a fresh ID map (`buildSubtreeNodeIdMap`), cloning nodes with optional class filtering, and relinking parent references under the target parent.

### Structural Reorganization

**`moveNode`** ([mutations.ts#L82-L112](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L82-L112)) safely relocates nodes while guarding against cycles using the `isAncestor` selector. It detaches the node from its old parent, updates `parentId`, and inserts at `newIndex`.

**`moveNodes`** ([mutations.ts#L136-L99](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L136-L99)) batches moves by reducing selections to top-level IDs, preventing descendants from being orphaned, and inserting consecutive nodes into the new parent.

**`wrapNode`** and **`wrapNodes`** ([mutations.ts#L88-L126](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts#L88-L126)) create container nodes. Single-node wrapping swaps the container into the original node's position; multi-selection wrapping finds the closest common ancestor before performing the insertion.

## The Dispatcher: applyTreeOperation

The `applyTreeOperation(tree, op)` function at the bottom of [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) serves as the single entry point for all mutations. It receives a discriminated union `TreeOperation` (defined in [`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts)) and switches on `op.kind` to route to the appropriate helper.

The function returns:

```typescript
{
  tree: NodeTree<PageNode>;
  affectedNodeIds: string[];
}

```

The `affectedNodeIds` array enables precise UI invalidation and cache updates. Because `applyTreeOperation` is a pure function that mutates the tree in place (for Mutative compatibility), callers requiring immutable snapshots must `structuredClone` the tree before invocation.

## Safety Mechanisms and Selectors

Mutation safety relies on [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts), which provides:

- **`getParent`**: O(1) parent retrieval via the flat map
- **`isAncestor`**: Cycle detection by traversing parent chains
- **`collectSubtreeIds`**: Recursive collection for bulk deletion

These utilities ensure that `moveNode` cannot create circular parent references and that `deleteNode` properly garbage collects descendant nodes.

## Integration with Editor and Plugin Sandbox

### Zustand Store Integration

The visual editor invokes mutations through `mutateActiveTree` in [`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), which wraps the dispatcher in Zustand's Mutative middleware:

```typescript
import { useSiteStore } from '@admin/store';
import { createNode } from '@core/page-tree';

function addTextBlock(parentId: string) {
  const node = createNode('core.text', { text: 'New block' });
  useSiteStore.getState().insertNode(node, parentId);
}

```

Under the hood, `insertNode` calls `mutateActiveTree((tree) => insertNode(tree, node, parentId))`, ensuring state updates remain immutable while the mutation logic stays pure.

### Plugin Sandbox Integration

Plugins running in the QuickJS VM serialize `TreeOperation` objects across the sandbox boundary. The MCP API accepts mutation batches:

```typescript
await api.cms.content.tree.mutate([
  { kind: 'renameNode', nodeId: 'abcd1234', name: 'Header' },
  { kind: 'setBreakpointOverride', nodeId: 'abcd1234', breakpoint: 'mobile', props: { color: '#f00' } },
]);

```

These operations deserialize to the same `TreeOperation` types consumed by `applyTreeOperation`, keeping plugin code synchronized with the core editor state.

## Direct Mutation Usage

For server-side scripts or tests, apply mutations directly:

```typescript
import { applyTreeOperation } from '@core/page-tree';
import type { TreeOperation } from '@core/page-tree';
import { parsePageNodeTree } from '@core/page-tree';

const rawTree = /* JSON from DB */;
const tree = parsePageNodeTree(rawTree);

const op: TreeOperation = {
  kind: 'insertNode',
  parentId: tree.rootNodeId,
  index: 0,
  node: {
    id: '', // ID generated internally
    moduleId: 'core.text',
    props: { text: 'Hello' },
    breakpointOverrides: {},
    children: [],
    classIds: [],
    parentId: null,
  },
};

const result = applyTreeOperation(tree, op);
console.log(result.affectedNodeIds); // ['<rootId>', '<newNodeId>']

```

## Summary

- Instatic uses a **flat node map** (`NodeTree<PageNode>`) with `parentId` references rather than nested objects, enabling O(1) lookups
- All mutations live in **[`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts)** as 11 pure functions compatible with the Mutative library
- The **`applyTreeOperation`** dispatcher in the same file routes discriminated union operations from [`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts)
- **Safety selectors** in [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) prevent cycles and manage subtree operations
- Both the **Zustand store** (`mutateActiveTree`) and **plugin VM** use the same dispatcher, ensuring consistency across the editor, server, and sandbox
- Mutations return `affectedNodeIds` for targeted UI updates while maintaining purity through `structuredClone` when immutability is required

## Frequently Asked Questions

### How does Instatic prevent circular references when moving nodes?

The `moveNode` function uses the `isAncestor` utility from [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) to verify that the target parent is not a descendant of the node being moved. This check runs before any `parentId` updates occur, ensuring the tree remains acyclic while maintaining O(1) lookup performance.

### What is the difference between duplicateNode and pasteSubtree?

**`duplicateNode`** clones an existing node within the same tree, generating fresh IDs and inserting the copy adjacent to the source. **`pasteSubtree`** handles foreign tree insertion (e.g., from clipboard), rebuilding the entire ID map via `buildSubtreeNodeIdMap` and optionally filtering class IDs to prevent style leakage between disconnected components.

### Why does applyTreeOperation return affectedNodeIds instead of a new tree?

The function mutates the tree in place for performance (Mutative compatibility), returning the modified tree reference and an `affectedNodeIds` array. This allows callers to perform selective UI updates or cache invalidation without deep equality checks on the entire node map, while callers needing immutability can wrap the call with `structuredClone`.

### How do plugins safely mutate the page tree without direct state access?

Plugins running in the QuickJS sandbox serialize `TreeOperation` objects through the MCP API. These operations deserialize to the same discriminated union consumed by `applyTreeOperation`, ensuring plugins execute identical mutation logic as the visual editor while remaining isolated from the host environment's memory space.