The mesh graph is a hierarchical, self-describing data store where types are not just schema annotations — they are living data elements that configure hubs, drive rendering, and version independently of one another. Understanding this model unlocks the rest of MeshWeaver's architecture.


Core Concepts

The MeshNode

Every element in the mesh — data records, type definitions, users, threads, configuration — is a MeshNode:

public record MeshNode(
    [property: Key] string Id,                       // Local identifier within its namespace
    [property: Editable(false)] string? Namespace = null   // Parent path (null/empty at the root)
)
{
    // A root-level node's Path is just its Id — no leading separator.
    public string Path => string.IsNullOrEmpty(Namespace) ? Id : $"{Namespace}/{Id}";
    public string? NodeType { get; init; }       // Path of this node's type definition
    public object? Content { get; init; }        // Typed payload
}

The Path is the node's unique address in the mesh. It is also how the portal builds URLs: navigate to https://your-host/Insurance/Claims/CLM-2024-001 and the mesh resolves, instantiates, and renders that exact node.

Concrete examples:

Id Namespace Resulting Path
Submissions Insurance/Underwriting Insurance/Underwriting/Submissions
CLM-2024-001 Insurance/Claims Insurance/Claims/CLM-2024-001
Planning Finance Finance/Planning

Namespace Hierarchy

Namespaces follow the pattern a/b/c/d, forming a tree rooted at /. Here is an insurance company example:

flowchart TB Root["/"] --> UW[Underwriting] Root --> Claims[Claims] Root --> Finance[Finance] Root --> Reserving[Reserving] UW --> Submissions[Submissions] UW --> Contracts[ContractManagement] Claims --> Open[OpenClaims] Claims --> Closed[ClosedClaims] Finance --> Close[Close] Finance --> Planning[Planning]

Each level is itself a node with its own type, content, and access policy. The hierarchy is not just organizational sugar — it determines hub instantiation, access inheritance, and query scope.

Path Navigation

Path Namespace Id
Underwriting (null/empty — root) Underwriting
Underwriting/Submissions Underwriting Submissions
Claims/CLM-2024-001 Claims CLM-2024-001
Finance/Close Finance Close

A node's place in the graph is defined by its path, so its edges are its real ancestors (above) and the real nodes below it. But a path segment is not necessarily a node: a/b/node can exist with neither a nor a/b being a real node — they are pure namespace groupings. Navigation must skip those empty segments.

The two query scopes that walk the graph's edges:

Direction Scope Returns
Above path:{p} scope:ancestors the real ancestor nodes (empty segments are absent — they are not nodes)
Below namespace:{p} scope:nextLevel the next populated level: the nearest real nodes below p, skipping empty intermediate segments

scope:nextLevel (the populated frontier) returns each node strictly below p for which no other node sits between it and p. So at the root of the example above, nextLevel returns Underwriting, Claims, Finance, Reserving; but if only Underwriting/Submissions/Q1 existed (with Submissions not a real node), nextLevel of Underwriting would surface Underwriting/Submissions/Q1 directly. On Postgres this is one indexed anti-join — see Query Syntaxscope:nextLevel. The Search area's graph navigator (Mesh Search) renders exactly these two edges and re-roots on click.


Types as First-Class Data

The central idea: In MeshWeaver, data types are data. A Claim type is not just a class or a schema file — it is a node stored in the mesh at a known path, queryable like any other node.

When you define a type like Claim, it lives at Type/Claim and configures every instance node that references it.

flowchart LR subgraph Path["Namespace Path"] A[Insurance] --> B[Claims] --> C[CLM-2024-001] end subgraph Types["Attached Types"] T1[BusinessUnit] -.->|nodeType| A T2[Department] -.->|nodeType| B T3[Claim] -.->|nodeType| C end

NodeTypes Are Nodes Too

Because a type is a node, you interact with it using exactly the same tools as any data node:

This is what "self-describing" means in practice: to understand how Insurance/Claims/CLM-2024-001 behaves, open its nodeTypeType/Claim — and read the same fields you would read on any other node.

NodeType Configuration

Each NodeType node specifies:

Component Purpose
Data Model Field definitions and validation rules
Views Layout configurations for different rendering contexts
Handlers Custom message handlers registered on the node's hub
Hub Configuration How to assemble the MessageHub for instances

The source files live alongside the type definition:

Type/
  Claim/
    Source/
      dataModel.json    ← Field definitions (claimNumber, lossDate, status, …)
      views.json        ← UI layouts (ClaimDetail, ClaimSummary, ClaimEdit)

Type Attachment

Any node can reference a type via its nodeType field:

{
  "id": "CLM-2024-001",
  "namespace": "Insurance/Claims",
  "nodeType": "Type/Claim",
  "content": {
    "claimNumber": "CLM-2024-001",
    "policyNumber": "POL-12345",
    "lossDate": "2024-03-15",
    "status": "Open",
    "reserveAmount": 50000
  }
}

The referenced type determines:


Semantic Versioning

The hierarchical namespace provides a natural home for semantic versioning. Breaking changes live in a sibling subtree, and both versions serve traffic simultaneously until migration is complete.

Insurance/
  ClaimsProcessing/
    V1/                  ← Version 1 of the domain
      Claim
      Reserve
    V2/                  ← Version 2 with breaking changes
      Claim
      Reserve
      Subrogation        ← New entity introduced in V2

Why This Matters

  1. Parallel serving — V1 and V2 run side by side; clients migrate at their own pace.
  2. Gradual migration — redirect consumers one by one rather than coordinating a big-bang cutover.
  3. Type evolution — add fields, rename entities, split types — all without breaking existing references.
  4. API stability — old paths keep working as long as the V1 subtree exists.

Version Pattern

{Vendor}/{Domain}/V{Major}/

Examples:


Hub Instantiation

URL Path Insurance/Claims/… Resolve Node storage lookup Find NodeType follow nodeType field Build MessageHub Data Sources schema + fields Msg Handlers custom operations View Defs layout areas Child Hub Config nested paths Type/Claim nodeType = "NodeType" type node

Path resolution: the platform walks from URL to node to type definition, then assembles a fully-configured MessageHub.

When the platform resolves a path, it follows a deterministic sequence:

  1. Resolve the node from the path (storage lookup or virtual template match).
  2. Locate the NodeType referenced by nodeType.
  3. Build a MessageHub configured with:
    • Data sources for the type's schema
    • Registered message handlers
    • View definitions for layout areas
    • Child hub configuration for nested paths

Template Nodes

Nodes can serve as templates for virtual instances. Rather than pre-creating thousands of leaf nodes, a single ancestor node backs every path beneath it: resolution walks the path and takes the longest existing prefix, reporting how many segments it consumed (IStorageAdapter.FindBestPrefixMatch(MeshNode? Node, int MatchedSegments); ResolvePath builds on it). So a Type/Claim-typed node at Insurance/Claims serves

with the unmatched tail carried as the virtual instance's key. Virtual nodes inherit the template's full hub configuration; the mesh instantiates them on demand — no pre-population required.

⚠️ addressSegments is not a real field. It appears in some older sample node JSON (samples/Graph/Data/*.json), but MeshNode declares no such property and the hub's UnmappedMemberHandling = Skip means it is parsed and discarded. Setting it changes nothing — the prefix walk above is the actual mechanism.


Query Patterns

The hierarchy is fully queryable. See Unified Path for the complete syntax reference.

// Direct children of Claims
namespace:Insurance/Claims

// All descendants, recursively
path:Insurance scope:descendants

// Find by type across the entire mesh
nodeType:Type/Claim

// Combine filters
namespace:Insurance/Claims nodeType:Type/Claim status:Open

See also: Query Syntax Reference


Summary: Why This Model Works

Property What It Enables
Self-describing Types are queryable data — no out-of-band schema registry needed
Flexible hierarchy Any depth, any branching factor; the tree fits the domain
Built-in versioning Semantic versions are just namespace segments
Dynamic configuration Change type behaviour by updating the type node — no code deployment
Discoverability Browse and link to types exactly as you would browse to data
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.