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:
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 |
Navigating along the edges — the populated frontier
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 Syntax → scope: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
Claimtype 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.
NodeTypes Are Nodes Too
Because a type is a node, you interact with it using exactly the same tools as any data node:
- It has a path (
Type/Claim), aName, anIcon, andContent(itsNodeTypeDefinition). Its ownnodeTypeis the literal"NodeType". - You can navigate to it in the portal, query for it (
nodeType:NodeType), version it, and grant fine-grained access on it — the same machinery that serves data instances. - A node's
nodeTypefield is the path of the type node, not an opaque string tag. Follow it to reach the full definition. The portal's Settings → Metadata view renders it as a clickable link to the type'sConfigurationarea.
This is what "self-describing" means in practice: to understand how Insurance/Claims/CLM-2024-001 behaves, open its nodeType — Type/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:
- Which fields are valid (data model)
- How the node renders in the portal (views)
- What operations are available (handlers and hub configuration)
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
- Parallel serving — V1 and V2 run side by side; clients migrate at their own pace.
- Gradual migration — redirect consumers one by one rather than coordinating a big-bang cutover.
- Type evolution — add fields, rename entities, split types — all without breaking existing references.
- API stability — old paths keep working as long as the V1 subtree exists.
Version Pattern
{Vendor}/{Domain}/V{Major}/
Examples:
Insurance/ClaimsProcessing/V1Insurance/Underwriting/V2MeshWeaver/Core/V3
Hub Instantiation
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:
- Resolve the node from the path (storage lookup or virtual template match).
- Locate the NodeType referenced by
nodeType. - Build a
MessageHubconfigured 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
Insurance/Claims/CLM-2024-001Insurance/Claims/CLM-2024-002
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.
⚠️
addressSegmentsis not a real field. It appears in some older sample node JSON (samples/Graph/Data/*.json), butMeshNodedeclares no such property and the hub'sUnmappedMemberHandling = Skipmeans 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 |