# Instatic Modules vs Visual Components: Architecture and Usage Differences

> Explore Instatic modules vs Visual Components. Learn how Instatic modules render static HTML and Visual Components offer reusable sub-trees. Understand the architecture and usage differences for CoreBunch/Instatic.

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

---

**Instatic modules are atomic leaf nodes rendered by the module-engine as static HTML, while Visual Components are complete reusable sub-trees with named slots stored in the `components` system table.**

In the CoreBunch/Instatic codebase, the editor and publisher distinguish between two types of reusable building blocks that serve different architectural purposes. While both participate in the same page-tree infrastructure, **modules** function as low-level rendering primitives, whereas **Visual Components** operate as higher-level composite patterns with structured parameter binding.

## Conceptual Overview

Understanding the fundamental abstraction layers helps clarify when to implement each construct.

### What Are Instatic Modules?

A **module** is an atomic leaf node identified by a `moduleId` that produces static HTML and CSS. Defined in `src/modules/*`, each module exports a `ModuleDefinition<TProps>` containing a `render()` function that returns HTML strings and optionally a React component for editor-time interaction.

Modules are the primary primitive for the **module-engine** and represent simple UI elements like buttons, headings, or images. They declare parameters as simple scalar values (strings, numbers, booleans) through a `schema` field, but they **do not support child nodes or slots**—they are strictly leaf nodes in the tree.

### What Are Visual Components?

A **Visual Component** is a complete sub-tree stored as a row in the `components` system table. Defined by a `VisualComponentSchema` in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts), a VC contains a full `NodeTree<VCNode>` that can nest multiple **BaseNode** entries (such as `base.container` or `base.heading`) and includes a `params` array supporting complex types like `slot`, `richText`, and `image`.

Visual Components expose **named slots** by inserting `base.slot-outlet` nodes within their tree structure. When used on a page, they are referenced via `base.visual-component-ref` nodes that automatically generate `base.slot-instance` children to hold consumer content.

## Architectural Differences

The distinction between these building blocks becomes clear when examining their file locations, data structures, and parameter handling.

### File Locations and Registration

**Modules** reside in `src/modules/*` with one folder per first-party module. Registration occurs through the singleton registry in [`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts):

```typescript
// src/modules/base/heading/index.ts
import type { ModuleDefinition } from '@core/module-engine';
import { registry } from '@core/module-engine';

export const HeadingModule: ModuleDefinition<{ text: string; level: number }> = {
  moduleId: 'base.heading',
  name: 'Heading',
  schema: {
    text: { type: 'string', defaultValue: '' },
    level: { type: 'number', defaultValue: 2 },
  },
  render: ({ text, level }) => `<h${level}>${text}</h${level}>`,
};

registry.registerOrReplace(HeadingModule);

```

**Visual Components** live under `src/core/visualComponents/*`, with schemas defined in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts) and slot synchronization logic in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts).

### Data Structures and Schemas

Modules export a flat definition with a render function and simple props schema. In contrast, Visual Components store entire node trees:

```typescript
// src/core/visualComponents/schemas.ts (structure)
export const VisualComponentSchema = Type.Object({
  id: Type.String(),
  name: Type.String(),
  tree: NodeTreeSchema,            // the VC's node tree
  params: Type.Array(VCParamSchema),
});

```

A Visual Component row in the `components` table contains a complete tree definition:

```jsonc
{
  "id": "vc-card",
  "name": "Card",
  "tree": {
    "nodes": {
      "root": { "moduleId": "base.body", "children": ["c1"] },
      "c1":   { "moduleId": "base.container", "children": ["h", "s"] },
      "h":    { "moduleId": "base.heading",    "props": { "text": "Card Title" } },
      "s":    { "moduleId": "base.slot-outlet", "props": { "slotName": "content" } }
    },
    "rootNodeId": "root"
  },
  "params": [
    { "id": "p1", "name": "title", "type": "string", "defaultValue": "Card Title" },
    { "id": "p2", "name": "content", "type": "slot" }
  ]
}

```

### Parameters and Slots

**Module parameters** are scalar values defined in the `ModuleDefinition` schema. Modules cannot contain child nodes or slots—they are atomic by design.

**Visual Component parameters** use the `VCParam` type, supporting `string`, `number`, `boolean`, `url`, `enum`, `color`, `image`, `richText`, and `slot` types. Slots are declared not as parameters, but by inserting `base.slot-outlet` nodes in the VC tree. When a VC reference appears on a page, `syncSlotInstances` automatically generates corresponding `base.slot-instance` nodes as children of the reference node:

```jsonc
// In a page's node map – a visual-component-ref node
{
  "id": "ref1",
  "moduleId": "base.visual-component-ref",
  "props": { "componentId": "vc-card", "instanceProps": { "title": "My Card" } },
  "children": ["slot1"]
}

```

After synchronization, the children become:

```jsonc
[
  {
    "id": "slot1",
    "moduleId": "base.slot-instance",
    "props": { "slotName": "content", "locked": true },
    "children": ["p1"]
  }
]

```

## Runtime Behavior

The rendering pipelines differ significantly between these two systems.

### Module Rendering Pipeline

Modules are rendered by the **module-engine** through `renderStandardNode`. The module's `publishBehavior` can be `standard`, `special`, or `transparent`. The render function executes during both editor and publisher phases, outputting static HTML directly.

### Visual Component Rendering Pipeline

Visual Components are inlined by the **publisher** using [`src/core/publisher/renderVisualComponentRef.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderVisualComponentRef.ts). The `renderVisualComponentRef` function instantiates the VC tree at the reference point, substitutes `propBindings` with `instanceProps`, and walks the resulting tree to emit static HTML:

```typescript
// src/core/publisher/renderVisualComponentRef.ts (excerpt)
export const renderVisualComponentRef = (refNode, vc, publishContext) => {
  const instantiatedTree = instantiateVCAtRef(refNode, vc);
  // Walk the instantiated tree, substitute propBindings → instanceProps
  // … emit static HTML for each node …
};

```

Unlike modules, VCs are not rendered as single nodes but expanded in-place to their full tree structure during the publishing phase.

## Mutations and State Management

Both modules and Visual Components mutate through the generic **tree-mutation API** (`insertNode`, `updateNodeProps`, etc.). However, Visual Components require additional synchronization logic.

When a VC definition changes, the system generates specific operations (`InsertSlot`, `RenameSlot`, `DeleteSlot`) to maintain consistency. The `syncSlotInstances` function in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) ensures that `base.visual-component-ref` nodes maintain the correct `base.slot-instance` children matching the VC's current slot definitions, preserving ordering and content integrity.

## Summary

- **Modules** are atomic leaf nodes in `src/modules/*` that export a `ModuleDefinition` with scalar parameters and a `render()` function—ideal for simple UI primitives like headings and buttons.
- **Visual Components** are reusable sub-trees stored in the `components` table with full `NodeTree<VCNode>` structures, supporting named slots via `base.slot-outlet` and complex parameter binding.
- **Modules** are rendered directly by the module-engine as static HTML, while **Visual Components** are inlined by the publisher through [`renderVisualComponentRef.ts`](https://github.com/CoreBunch/Instatic/blob/main/renderVisualComponentRef.ts) during the build process.
- **File locations**: Modules register in [`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts); Visual Components define schemas in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts) and synchronize slots via [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts).
- **Slot handling** is exclusive to Visual Components, automatically managed through `base.slot-instance` nodes that mirror the VC's `base.slot-outlet` definitions.

## Frequently Asked Questions

### When should I use a module versus a Visual Component?

Use a **module** when building simple, atomic UI elements like buttons, headings, or images that require only scalar parameters and render as single HTML elements. Use a **Visual Component** when creating composite, reusable patterns like cards or dialogs that need multiple nested nodes, named slots for content injection, and complex parameter binding across the entire sub-tree.

### How do Visual Component slots differ from module children?

Modules cannot have children in the page tree—they are strictly leaf nodes. Visual Components expose **named slots** by placing `base.slot-outlet` nodes in their tree definition. When a VC is referenced on a page via `base.visual-component-ref`, the system automatically generates `base.slot-instance` nodes as children of that reference, creating the insertion points where page editors can add content. This contrasts with modules, which must handle any internal structure within their own `render()` logic.

### Can Visual Components be nested inside other Visual Components?

While Visual Components contain standard **BaseNode** entries (such as `base.container` or `base.heading`) in their `NodeTree`, the architecture stores VCs as complete sub-tree definitions in the `components` table. Whether a VC can reference another VC depends on the publisher's `renderVisualComponentRef` implementation, which handles the inlining process. The slot synchronization logic in `syncSlotInstances` manages the tree structure but does not inherently prevent nesting if the VC definition includes another `base.visual-component-ref` node.

### Where are Visual Component definitions stored compared to modules?

**Module definitions** live as TypeScript files in `src/modules/*` and register at runtime through [`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts). **Visual Component definitions** persist as JSON rows in the `components` system table, with their schemas validated by `VisualComponentSchema` in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts). This distinction means modules are typically code-first constructs deployed with the application, while Visual Components can be stored and retrieved dynamically from the database.