# How Instatic's NodeTree<TNode Primitive Powers Pages, Components, and Slot Fills

> Discover Instatic's NodeTree<TNode> primitive for unifying pages and components. Learn how its flat, ID-keyed structure enables type-safe mutations and O(1) lookups for efficient development.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-30

---

**Instatic's `NodeTree<TNode>` is a flat, ID-keyed tree structure that unifies pages, visual components, and slot fills under a single generic type, enabling type-safe mutations and O(1) node lookups through a shared `nodes` map and `rootNodeId` pattern.**

The **CoreBunch/Instatic** repository implements every hierarchical document as a generic `NodeTree<TNode>`. This architectural primitive eliminates duplication between pages and visual components while maintaining strict TypeScript type safety through its parameterized node types.

## The Flat Tree Architecture of Instatic NodeTree

At its core, `NodeTree<TNode>` abandons traditional nested JSON structures in favor of a **flat map**. The shape consists of two required fields: a `nodes` dictionary keyed by string IDs, and a single `rootNodeId` string that serves as the entry point for traversal.

In [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts), the schema defines this structure using TypeBox:

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

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

```

This flat architecture delivers three critical advantages. **O(1) node lookup** eliminates recursive traversal when accessing specific nodes. **Cyclic-safe walks** become trivial because parent-child relationships are reconstructed via ID references rather than object nesting. **Database serialization** requires no custom transformers since the structure is already normalized for storage.

## Generic Type Safety with NodeTree<TNode>

The `TNode` generic parameter allows the same tree container to hold different node shapes while preserving compile-time type safety. The interface defaults to `BaseNode` but accepts specializations like `PageNode` or `VCNode` (Visual Component Node).

Runtime validation operates only on the shared `BaseNode` shape through `NodeTreeSchema`. The mutation layer trusts the TypeScript generic type for development ergonomics, while the persistence layer validates against the schema-boundary. This separation means `NodeTree<PageNode>` and `NodeTree<VCNode>` share identical runtime footprints but offer distinct autocomplete and type-checking during development.

## Three Applications: Pages, Visual Components, and Slot Fills

### Pages as NodeTree<PageNode>

In [`src/core/page-tree/page.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/page.ts), a `Page` structurally **is** a `NodeTree<PageNode>` extended with metadata fields. The schema inherits directly from `NodeTreeSchema`:

```typescript
export const PageSchema = Type.Object({
  ...NodeTreeSchema.properties,
  nodes: Type.Record(Type.String(), PageNodeSchema),
  id: Type.String(),
  slug: Type.String(),
  title: Type.String(),
})

```

By spreading `NodeTreeSchema.properties` and overriding the `nodes` record with `PageNodeSchema`, Instatic guarantees that any `Page` instance can be treated as a generic `NodeTree<BaseNode>` while retaining page-specific node capabilities.

### Visual Components and VCNode Trees

Visual components store their hierarchical definition in a `NodeTree<VCNode>` accessible via `vc.tree`. The builder logic in [`src/core/plugin-sdk/builders/tree.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/builders/tree.ts) flattens nested component definitions into this standardized shape. The `VCNode` type—defined in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts)—extends `BaseNode` with component-specific properties while conforming to the same flat-map structure.

### Slot Fills and Subtree Injection

When a visual component is dropped into a page slot, Instatic creates a `base.slot-instance` node inside the consumer page's tree. The children of this node form a subtree that follows the identical `NodeTree` shape, allowing slot fills to leverage the same traversal and mutation logic as root-level documents.

## Generic Traversal and Mutation APIs

### Reading with Selectors

The [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) module exports generic functions like `getNode` and `getChildren` that operate on any `NodeTree<TNode>`. Because selectors interact only with the shared tree structure, they work identically across pages and components:

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

function listAllHeadings<T extends BaseNode>(tree: NodeTree<T>): string[] {
  const walk = (nodeId: string, out: string[] = []): string[] => {
    const node = getNode(tree, nodeId)
    if (!node) return out
    if (node.type === 'base.heading') out.push(node.props.text ?? '')
    for (const child of getChildren(tree, nodeId)) {
      walk(child.id, out)
    }
    return out
  }
  return walk(tree.rootNodeId)
}

```

### Writing with Mutations

All tree mutations accept a generic `NodeTree<TNode>` draft wrapped by `mutateActiveTree`. The [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) file implements primitives like `insertNode` that remain agnostic to the concrete node type:

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

function addButtonToVC(vcTree: NodeTree<VCNode>, parentId: string) {
  mutateActiveTree(vcTree, draft => {
    insertNode(draft, {
      id: 'button-1',
      type: 'base.button',
      props: { label: 'Click me' },
      children: [],
    }, parentId)
  })
}

```

The same `insertNode` function works for pages when passed a `NodeTree<PageNode>` draft, with TypeScript inferring the appropriate generic constraint.

## Validation and Persistence

Before storage, trees undergo validation against `NodeTreeSchema` using TypeBox helpers. This occurs at the persistence boundary, ensuring that serialized data conforms to the expected flat-map structure regardless of the specific `TNode` specialization used during runtime operations.

```typescript
import { NodeTreeSchema } from '@core/page-tree/treeSchema'
import { compiledEncode } from '@core/utils/typeboxHelpers'

function persistTree<T extends BaseNode>(tree: NodeTree<T>) {
  return compiledEncode(NodeTreeSchema, tree)
}

```

## Summary

- **Instatic NodeTree** uses a flat `Record<string, TNode>` plus `rootNodeId` instead of nested objects, enabling O(1) lookups and safe serialization.
- The generic `TNode` parameter provides type-specific operations for `PageNode`, `VCNode`, and `BaseNode` while sharing runtime validation logic.
- Pages extend `NodeTree` directly in [`src/core/page-tree/page.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/page.ts), while visual components embed trees via `vc.tree` as defined in the plugin SDK.
- Slot fills inject subtrees using the same `NodeTree` shape, maintaining consistency across document boundaries.
- Generic selectors and mutations in [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) and [`mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/mutations.ts) operate on any tree specialization without type-specific branching.

## Frequently Asked Questions

### What is the advantage of a flat nodes map over a nested tree structure?

The flat `nodes` map provides **O(1) node lookup** by ID, eliminates the risk of circular references during serialization, and simplifies parent-index reconstruction. This design makes deep traversal operations like `flattenSubtree` safer and reduces database storage complexity since the structure maps directly to JSON columns without custom transformers.

### How does Instatic ensure type safety when the same mutations work for pages and components?

Instatic leverages TypeScript generics to enforce type safety at compile time while using a shared runtime schema. The `NodeTree<TNode>` interface accepts type parameters like `PageNode` or `VCNode`, allowing `insertNode` and `getChildren` to return correctly typed objects. Runtime validation against `NodeTreeSchema` occurs only at persistence boundaries, ensuring data integrity without sacrificing development ergonomics.

### Can NodeTree<TNode> handle deeply nested component hierarchies?

Yes. The flat-map structure imposes no depth limits on logical nesting. Parent-child relationships are tracked via ID references in the `children` arrays of each node, not by object nesting. The [`src/core/plugin-sdk/builders/tree.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/builders/tree.ts) builder flattens arbitrarily deep component hierarchies into the `NodeTree<VCNode>` format while preserving the original structural semantics.

### Where is the NodeTree schema validated in the Instatic codebase?

Validation occurs at the persistence boundary using `NodeTreeSchema` defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts). The `compiledEncode` helper from `@core/utils/typeboxHelpers` validates tree structures before database storage. The mutation layer operates on TypeScript interfaces and trusts the pre-validated data, creating a clear separation between compile-time type checking and runtime validation.