# Understanding the Instatic Page Tree Schema (NodeTree): Structure and Usage

> Explore the Instatic page tree schema (NodeTree). Learn how this flat map of nodes powers validation, traversal, mutation, and persistence in the CMS. Get a clear understanding of its structure and usage.

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

---

**Instatic models every hierarchical document—pages, visual components, and slot fills—as a single primitive called a NodeTree, defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts) as a flat map of nodes plus a root node ID, which powers validation, traversal, mutation, and persistence across the CMS.**

The **page tree schema**—known internally as the **NodeTree**—is the foundational data structure in CoreBunch/Instatic that unifies how hierarchical content is stored and manipulated. Rather than maintaining separate shapes for pages, visual components, and slot fragments, the codebase relies on one canonical flat-map structure declared in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts) and consumed by validation, mutation, selector, and storage layers.

## What Is the Instatic Page Tree Schema?

At its core, the page tree schema is an interface and runtime validator that represents any tree of nodes as two fields. In [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts), the runtime schema is exported as `NodeTreeSchema`:

```typescript
export const NodeTreeSchema = Type.Object({
  nodes: Type.Record(Type.String(), BaseNodeSchema),
  rootNodeId: Type.String(),
})

```

A matching generic TypeScript interface provides compile-time type safety:

```typescript
export interface NodeTree<TNode extends BaseNode = BaseNode> {
  nodes: Record<string, TNode>
  rootNodeId: string
}

```

This generic accepts a node type—such as `PageNode` or `VCNode`—so that mutation utilities can preserve richer type information while the persisted payload is validated against `BaseNodeSchema`.

## Key Design Principles

- **Unified primitive** – As implemented in CoreBunch/Instatic, a `Page` is a `NodeTree<PageNode>` that adds metadata such as `id`, `slug`, and `title` according to [`src/core/page-tree/page.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/page.ts). A visual component exposes its structure as `vc.tree`, a `NodeTree<VCNode>`. Slot fills appear as children of a `base.slot-instance` node inside the consumer page’s tree.
- **Flat-map storage** – Every node lives in a single `Record<string, BaseNode>` keyed by ID, giving **O(1)** node lookup regardless of tree depth.
- **Type-safe extensibility** – New node sub-types extend `BaseNode` without requiring changes to core mutation logic because the APIs operate on the generic `NodeTree<TNode>` shape.

## How the NodeTree Is Used Across the Codebase

### Validation at the Persistence Boundary

Before a stored tree reaches the mutation API, it must pass through [`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts). Functions such as `assertValidNodeTree` and `parsePageNodeTree` decode raw JSON against `NodeTreeSchema` and raise detailed validation errors when the shape is incorrect. This guards the boundary between storage or network payloads and in-memory operations.

### Tree Mutations

All write operations in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) accept a `NodeTree<PageNode>` draft and apply changes such as `insertNode`, `renameNode`, and `moveNode`. Because these utilities are implemented against the generic `NodeTree<TNode>` interface, the same logic is reused for visual component trees without code duplication.

### Selectors and Read-Only Traversal

The [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) module exports helpers like `getNode`, `getChildren`, `flattenSubtree`, and `isAncestor`. These functions operate on any `NodeTree<TNode>`, providing a unified way to read hierarchical relationships without mutating state.

### Persistence and Database Storage

As defined in [`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts), the `pages` column stores a JSON-encoded `NodeTree<PageNode>`. This flat-map shape is also used for HTML imports, plugin SDK builders, and the canvas renderer, ensuring that every layer of the CMS reads the same canonical structure.

## Practical Examples

### Creating a New Page Node Tree

```typescript
import { NodeTree } from '@core/page-tree'
import { BaseNode } from '@core/page-tree/baseNode'

function createEmptyPageTree(): NodeTree<BaseNode> {
  const rootId = 'root'
  return {
    nodes: {
      [rootId]: { id: rootId, type: 'base.body', props: {}, children: [] },
    },
    rootNodeId: rootId,
  }
}

```

### Adding a Node via the Mutation API

```typescript
import { insertNode } from '@core/page-tree/mutations'
import type { NodeTree, PageNode } from '@core/page-tree'

function addParagraph(
  tree: NodeTree<PageNode>,
  parentId: string,
  paragraphId: string,
) {
  insertNode(tree, {
    id: paragraphId,
    type: 'base.paragraph',
    props: { text: 'Hello world' },
    children: [],
    parentId,
  })
}

```

### Reading a Node and Its Children

```typescript
import { getNode, getChildren } from '@core/page-tree/selectors'

const paragraph = getNode(tree, 'paragraph-1')
const childIds = getChildren(tree, paragraph?.id ?? '')

```

### Validating a Stored Tree on Load

```typescript
import { assertValidNodeTree } from '@core/page-tree'

function loadSiteTree(raw: unknown) {
  // Throws if the shape is incorrect
  assertValidNodeTree(raw as any, 'site.pages[0].tree')
  return raw as NodeTree<PageNode>
}

```

## Summary

- The **page tree schema** (`NodeTree`) in CoreBunch/Instatic is a flat-map structure composed of `nodes` and `rootNodeId`, defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts).
- A generic `NodeTree<TNode>` interface preserves type safety for pages, visual components, and slot fills while `BaseNodeSchema` validates runtime payloads.
- **Validation** utilities in [`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts) enforce structural correctness before mutations occur.
- **Mutation** helpers in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) draft changes across any `NodeTree<TNode>`, and **selector** utilities in [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) provide read-only traversal.
- The same JSON shape is persisted in the database via [`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts), powering imports, SDK builders, and the rendering canvas.

## Frequently Asked Questions

### What is the relationship between a Page and a NodeTree in Instatic?

According to [`src/core/page-tree/page.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/page.ts), a `Page` is implemented as a `NodeTree<PageNode>` that wraps the generic tree structure with page-level metadata such as `id`, `slug`, and `title`. This means every page stores its content hierarchy inside the same flat-map primitive defined by the page tree schema.

### Why does Instatic use a flat map instead of a nested tree structure?

The flat `Record<string, BaseNode>` in the `nodes` field allows **O(1)** lookup by ID, which simplifies mutations and selector operations. Parent-child relationships are still expressed through each node’s `children` array, but the data itself is normalized for performance and easier patching.

### How does Instatic validate an incoming NodeTree before processing it?

As implemented in CoreBunch/Instatic, functions like `assertValidNodeTree` and `parsePageNodeTree` in [`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts) decode raw JSON against `NodeTreeSchema`. If the payload violates the expected shape, the validator throws a detailed error before the tree reaches the mutation layer.

### Can the same NodeTree logic be reused for visual components and slot fills?

Yes. Because the core utilities are generic over `NodeTree<TNode>`, visual components reuse the exact same mutation and selector logic via `NodeTree<VCNode>`. Slot fills are stored directly as children of a `base.slot-instance` node within a page’s tree, so they also conform to the same schema without requiring a separate data structure.