hexagon System Architecture

NodeDex

The Mesh Field Journal

NodeDex is Socialmesh's procedural identity and field journal system. Every node you discover earns a unique geometric sigil, a behavioral personality trait, and a patina score that deepens with real observation. No grinding, no gamification — just genuine mesh exploration rendered as a living archive.

14 Subsystems
9 Personality Traits
6 Patina Axes
5 Disclosure Tiers
4 Atmosphere Effects

Table of Contents

account_tree

Architecture Overview

NodeDex operates as an independent enrichment layer on top of the Meshtastic protocol stack. It reads node telemetry from the live nodesProvider but maintains its own persistence, identity generation, and visualization pipeline. The system is designed to be fully offline — Cloud Sync is optional and additive.

BLE / USB Transport
Protocol Layer
NodeDex Engines
SQLite Store
Riverpod Providers & UI

When a node appears or updates on the mesh, the NodeDexNotifier provider processes the event: records an encounter (subject to a cooldown), updates signal records, caches device info, derives region from GPS, and tracks co-seen relationships. All derived data — sigils, traits, patina, field notes — is computed lazily through pure-function engines with zero side effects.

info

Design principle: Every engine in NodeDex is a pure-function class with static methods. The same inputs always produce the same outputs. No randomness, no network calls, no async. This makes the system fully deterministic and trivially testable.

Key Source Paths

  • lib/features/nodedex/models/ — Data models (NodeDexEntry, SigilData, SigilEvolution, ImportPreview)
  • lib/features/nodedex/services/ — Engines (SigilGenerator, TraitEngine, PatinaScore, ProgressiveDisclosure, FieldNoteGenerator)
  • lib/features/nodedex/providers/ — Riverpod state management and derived providers
  • lib/features/nodedex/screens/ — UI screens (list, detail, constellation)
  • lib/features/nodedex/widgets/ — Reusable widgets (SigilCard, TraitBadge, PatinaStamp, etc.)
  • lib/features/nodedex/album/ — Collector Album system
  • lib/features/nodedex/atmosphere/ — Elemental Atmosphere particle system
  • lib/features/nodedex/constellation/ — Star-map visualization
data_object

Data Model

The NodeDexEntry is the central model — one instance per discovered node. It captures everything from first-seen timestamps to rolling encounter windows, signal records, co-seen relationship graphs, and cached device metadata. The model is independent of MeshNode; it reads from protocol data but persists its own enrichment layer.

Core Fields

Identity & Timestamps

  • nodeNum — Meshtastic node number (primary key)
  • firstSeen / lastSeen — Discovery and most recent observation timestamps
  • encounterCount — Total distinct encounter sessions (5-minute cooldown between counts)
  • lastKnownName — Cached display name, persists even when node goes offline
  • lastKnownHardware / lastKnownRole / lastKnownFirmware — Cached device metadata

Signal & Distance Records

  • maxDistanceSeen — Furthest recorded range in meters
  • bestSnr / bestRssi — All-time best signal quality records
  • messageCount — Messages exchanged with this node

User-Owned Data

  • socialTag — User-assigned classification (Contact, Trusted Node, Known Relay, Frequent Peer)
  • userNote — Free-text note (max 280 chars), never transmitted over mesh
  • Both fields carry updatedAtMs timestamps for last-write-wins conflict resolution during sync

Rich History

  • encounters — Rolling window of the most recent 50 EncounterRecords (timestamp, distance, SNR, RSSI, lat/lon)
  • seenRegions — List of SeenRegion records (regionId, label, firstSeen, lastSeen, encounterCount)
  • coSeenNodes — Map of CoSeenRelationship records (count, firstSeen, lastSeen, messageCount)
  • sigil — Cached SigilData for the procedural geometric identity

Social Tags

Users can classify discovered nodes with one of four social tags. Tags are private metadata — never transmitted over the mesh, but included in JSON exports and Cloud Sync.

Contact

A known person you communicate with

Trusted Node

Verified infrastructure relay or gateway

Known Relay

A node that reliably forwards traffic

Frequent Peer

Regularly co-seen on the mesh

Computed Properties

NodeDexEntry exposes several derived getters computed from the raw data:

pentagon

Sigil Generator

Every Meshtastic node receives a unique geometric sigil — a constellation-style procedural glyph generated entirely from its node number. The generation is deterministic: the same nodeNum always produces the same sigil. No randomness, no per-session variation, no network dependency.

Hash Function

The generator uses a murmur3-style integer hash finalizer for bit mixing. Multiple rounds of mixing extract independent parameters from a single 32-bit seed, ensuring that even sequential node numbers produce visually distinct sigils.

Mixing Algorithm

Each round applies: XOR right-shift by 16, multiply by 0x45d9f3b, mask to 32 bits. This provides good avalanche properties — a single bit change in the input flips roughly half the output bits. Five rounds (h0 through h4) are used to extract all sigil parameters.

Sigil Parameters

Vertices
3 – 8
Rotation
24 steps
Inner Rings
0 – 3
Radial Lines
on / off
Center Dot
on / off
Symmetry Fold
2 – 6

Color Palette

Each sigil receives a unique 3-color combination (primary, secondary, tertiary) drawn from a curated 16-color palette. The selection is hash-derived with collision avoidance — all three colors are guaranteed to be distinct.

Geometry Pipeline

The renderer calls computePoints() to get normalized vertex positions (unit circle) and computeEdges() to get the line connections. Points include outer polygon vertices, inner ring vertices (scaled and offset-rotated), and an optional center point. Edges connect polygon sides, inter-ring struts, and optional radial lines governed by the symmetry fold value.

info

No dart:math dependency: The generator uses Bhaskara I's sine approximation (accurate to ~0.2%) and derives cosine via phase shift. This keeps the sigil system zero-dependency and portable.

psychology

Trait Engine

Traits are passive personality classifications inferred from real observable data — encounter patterns, position history, uptime, role, and activity frequency. Traits are never user-editable. The engine runs pure functions that take a NodeDexEntry and optional live metrics to produce a ranked classification with confidence scores and human-readable evidence.

Inference Thresholds

Trait inference activates only after minimum thresholds are met:

  • Minimum encounters: 3 distinct sessions
  • Minimum age: 1 hour since first seen

Below these thresholds, the node is classified as Newcomer (Unknown trait) with confidence 0.

The Nine Traits

Relay

High throughput, forwards traffic

Role is ROUTER, ROUTER_CLIENT, REPEATER, or ROUTER_LATE with elevated channel utilization

Wanderer

Seen across multiple locations

3+ distinct positions or 2+ regulatory regions

Sentinel

Fixed position, long-lived guardian

10+ encounters over 3+ days from a stable position

Beacon

Always active, high availability

Encounter rate exceeding 8 sightings per day

Ghost

Rarely seen, elusive presence

Encounter rate below 0.3 per day relative to age

Courier

Carries messages across the mesh

High message-to-encounter ratio indicates deliberate data transport

Anchor

Persistent hub with many connections

High co-seen density, fixed position, central to local topology

Drifter

Irregular timing, fades in and out

Present but unpredictable, intervals vary widely with no periodicity

Newcomer

Recently discovered

Insufficient data to classify; assigned when thresholds are not met

Scoring & Evidence

Each trait is scored independently on a 0.0 to 1.0 confidence scale. The engine provides two APIs:

Evidence lines read like field journal entries: "Seen across 4 regions", "Encounter rate 12.3/day exceeds beacon threshold". A secondary trait is included when another trait scores above 0.5 alongside the primary.

auto_awesome

Patina Score

Patina is a deterministic digital history score (0–100) measuring how much observable history a node has accumulated in your field journal. It is not fake damage or cosmetic aging — it is a numerical measure of genuine documentation depth. A score of 0 means "just discovered, almost nothing known." A score of 100 means "deeply documented, rich history across time, space, and relationships."

Six Axes

Axis Weight Saturation Point Curve
Tenure 20% ~90 days known Asymptotic: 1 - e-3t/90
Encounters 20% ~60 encounters Logarithmic: ln(1+n) / ln(61)
Reach 15% 6 regions / 10 positions Blended log (60% regions, 40% positions)
Signal Depth 15% Has SNR + RSSI + encounter signals Additive: 0.3 SNR + 0.3 RSSI + 0.4 ratio
Social 15% 20 co-seen / 30 messages Blended log (60% co-seen, 40% messages)
Recency 15% ~24 hour half-life Exponential decay: e-0.693t/24

Each axis uses logarithmic or asymptotic curves so that early gains are meaningful but diminishing returns prevent runaway scores. A node seen 3 times over 2 days with 1 region scores ~15–25. A node seen 50 times over 30 days across 4 regions scores ~60–75.

Stamp Labels

The raw score maps to a human-readable stamp used in the UI:

Trace 0–9
Faint 10–24
Noted 25–39
Logged 40–54
Inked 55–69
Etched 70–84
Archival 85–94
Canonical 95–100
visibility

Progressive Disclosure

Not everything about a node is visible immediately. Progressive Disclosure controls what information is revealed based on how much observable history has accumulated. New nodes start sparse — only a sigil and basic metadata. As encounters, time, and data accumulate, additional layers are progressively unlocked. This creates a sense of earned knowledge.

Five Disclosure Tiers

Trace
Tier 0
  • Sigil visible
  • Name + hex ID
  • Nothing else
Noted
2+ enc, 1h+
  • Primary trait badge
Logged
5+ enc, 1d+
  • Trait evidence lines
  • Field note visible
Inked
10+ enc, 3d+
  • Full trait list
  • Patina stamp
  • Observation timeline
  • Identity overlay (15%)
Etched
20+ enc, 7d+
  • Full overlay density
  • Up to 40% opacity

The overlay density ramps smoothly based on data richness — encounter count, age, region diversity, and co-seen relationships all contribute. The DisclosureState object exposes boolean flags that the UI reads directly, keeping disclosure logic out of widget code.

upgrade

Sigil Evolution

Sigil Evolution adds subtle visual depth to a node's sigil based on patina score. It does not create a second scoring system — it derives everything from the existing patina (0–100). Visual effects are intentionally subtle: slight line thickening, gentle color deepening, and micro-etch density increases.

Five Maturity Stages

Seed
0 – 19
Weight: 1.00× Tone: 1.00× Detail: Tier 0
Marked
20 – 39
Weight: 1.05× Tone: 1.03× Detail: Tier 1
Inscribed
40 – 59
Weight: 1.10× Tone: 1.06× Detail: Tier 2
Heraldic
60 – 79
Weight: 1.15× Tone: 1.09× Detail: Tier 3
Legacy
80 – 100
Weight: 1.20× Tone: 1.12× Detail: Tier 4

Augment Marks

At Inscribed stage (tier 2) and above, tiny optional marks can appear near the sigil's outer ring, derived from the node's trait. Only one augment per sigil:

edit_note

Field Note Generator

Each node receives a deterministic, single-line field journal observation based on its identity seed, primary trait, and accumulated history. Notes are fully deterministic: the same node number and trait always produce the same note. They read like entries in a naturalist's field journal.

Example Field Notes

  • "Recorded across 4 regions. No fixed bearing." — Wanderer
  • "Steady signal. 12.3 sightings per day." — Beacon
  • "Rarely observed. Last confirmed sighting 3d ago." — Ghost
  • "Fixed position. Monitoring for 14 days." — Sentinel
  • "Forwarding traffic. Router role confirmed." — Relay
  • "Hub node. Co-seen with 8 other nodes." — Anchor
  • "Timing unpredictable. Appears and fades without pattern." — Drifter
  • "Recently discovered. Observation in progress." — Newcomer

The generator maintains 8 template families per trait (64+ total templates). Template selection is hash-derived from the node number, and interpolation fills in real values: {regions}, {encounters}, {rate}, {distance}, {lastSeen}, {coSeen}, etc.

style

Collector Album

The Collector Album presents discovered nodes as collectible trading cards in a grid layout, grouped by trait or rarity. Each card features the node's sigil with holographic shimmer effects scaled by rarity tier. The album uses a "mystery slot" pattern to suggest undiscovered nodes without implying a fixed total.

Grid Layout

Rarity Tiers

Common
Uncommon
Rare
Epic
Legendary

Holographic Effect

Rare, Epic, and Legendary cards receive an animated rainbow shimmer overlay rendered via CustomPainter. The effect uses a diagonal gradient with spectral color bands that drift slowly across the card surface, creating the look of a premium holographic trading card.

Holographic Parameters

  • Cycle duration: 2400ms per full shimmer sweep
  • Rare opacity: 12% — Epic: 18% — Legendary: 25%
  • Dual-pass rendering: Two gradient layers at different angles for depth
  • Blend mode: Screen (additive light)
  • Reduce-motion: Falls back to a static sheen at fixed phase
  • Mini cards: Simplified single-pass variant at 5000ms cycle for grid performance

Card Flip Animation

Cards flip with a 3D perspective transform (380ms, easeOutBack curve, 0.0025 perspective). The back face shows additional node metadata. Press feedback scales cards to 90% over 80ms.

Album Cover

The album opens with a cover section (260px tall) showing the user's Explorer Title emblem, discovery statistics, and a rarity breakdown bar. The cover animates in over 600ms.

grain

Elemental Atmosphere

The Elemental Atmosphere is an optional ambient visual system that renders data-driven particle effects behind NodeDex and map views. Effects are purely cosmetic, non-interactive, and never obstruct content. The system respects reduce-motion preferences and auto-throttles on low-end devices.

Four Effects

Rain

Vertical streaks tied to packet activity and node count. Cool blue-grey tones. Fall speed 120–280 px/s with slight horizontal drift.

Lifetime: 0.4–1.2s | Streak: 6–18px | Max alpha: 8%
Embers

Rising sparks tied to patina scores and relay contribution. Warm amber-orange with sinusoidal horizontal wander and glow pulsing.

Lifetime: 1.5–3.5s | Wander: 20px | Glow max: 12%
Mist

Drifting fog blobs tied to sparse data regions (ghost/unknown nodes). Translucent grey with slow lateral movement.

Lifetime: 3–7s | Radius: 30–80px | Max alpha: 5%
Starlight

Ambient twinkling background particles. Always gently present when enabled. Pale blue-white points with slow twinkle cycle.

Lifetime: 2–5s | Radius: 0.5–2px | Max alpha: 10%

Performance Budget

Particle Limits

  • Per effect: 80 particles max
  • Global ceiling: 200 particles across all effects
  • Target frame time: 12ms — auto-throttle after 5 consecutive slow frames
  • Spawn cooldown: 16ms minimum between spawns to prevent bursts
  • Object pool: 250 pre-allocated particles recycled to avoid GC pressure

Context Multipliers

Effect intensity is scaled by screen context:

storage

SQLite Storage

NodeDex data persists in a dedicated SQLite database (nodedex.db) managed by the NodeDexDatabase lifecycle class and the NodeDexSqliteStore read/write layer. The database is currently at schema version 4 with full forward migration support.

Schema

table_chart nodedex_entries
  • node_num INTEGER PK
  • first_seen_ms INTEGER
  • last_seen_ms INTEGER
  • encounter_count INTEGER
  • max_distance REAL
  • best_snr INTEGER
  • best_rssi INTEGER
  • message_count INTEGER
  • social_tag INTEGER
  • user_note TEXT
  • sigil_json TEXT
  • last_known_name TEXT
  • last_known_hardware TEXT
  • last_known_role TEXT
  • last_known_firmware TEXT
  • deleted INTEGER
table_chart nodedex_encounters
  • id INTEGER PK AUTO
  • node_num INTEGER FK
  • ts_ms INTEGER
  • distance_m REAL
  • snr REAL
  • rssi REAL
  • lat REAL
  • lon REAL
  • session_id TEXT
table_chart nodedex_seen_regions
  • node_num, region_key PK
  • label TEXT
  • first_seen_ms INTEGER
  • last_seen_ms INTEGER
  • count INTEGER
table_chart nodedex_coseen_edges
  • a_node_num, b_node_num PK
  • first_seen_ms INTEGER
  • last_seen_ms INTEGER
  • count INTEGER
  • message_count INTEGER
  • CHECK (a < b)
table_chart sync_outbox
  • id INTEGER PK AUTO
  • entity_type TEXT
  • entity_id TEXT
  • op TEXT
  • payload_json TEXT
  • attempt_count INTEGER
table_chart sync_state
  • key TEXT PK
  • value TEXT

Migration History

  • v1: Initial schema — entries, encounters, regions, co-seen edges, sync tables
  • v2: Added social_tag_updated_at_ms and user_note_updated_at_ms for Cloud Sync conflict resolution
  • v3: Added last_known_name to cache display names when nodes go offline
  • v4: Added last_known_hardware, last_known_role, last_known_firmware for offline device info display

Write Strategy

Writes use a debounced batch strategy: individual saveEntry() calls are queued and flushed together after a short delay. This reduces SQLite write amplification during rapid encounter processing. Critical writes (user edits, imports) use saveEntryImmediate() to bypass the debounce. Corruption is handled by deleting and recreating the database — data loss is acceptable since Cloud Sync provides the recovery path.

cloud_sync

Cloud Sync

Cloud Sync is an optional premium feature that backs up NodeDex data to Firestore and syncs across devices. The system uses an outbox pattern for writes and a watermark-based pull for reads. The app remains fully functional without Cloud Sync — all data lives in SQLite first.

Outbox Pattern

Write Flow

  1. Local mutation writes to SQLite
  2. An outbox record is enqueued in the sync_outbox table
  3. The sync service drains the outbox when enabled and network is available
  4. Each outbox entry is pushed to Firestore with retry logic (max 5 attempts)
  5. Successfully synced entries are removed from the outbox

Pull Flow

  1. The service queries Firestore for documents updated after the last watermark
  2. Watermarks are per-user (nodedex_last_pull_ms_<uid>) to prevent leaking between accounts
  3. Pulled entries are merged into SQLite using the same merge semantics as local merge
  4. User-owned fields (socialTag, userNote) use last-write-wins with per-field timestamps
  5. The watermark advances to the newest pulled timestamp

Conflict Resolution

Numeric fields use max-wins (encounterCount, messageCount, bestSnr, bestRssi). Timestamps use earliest for firstSeen and latest for lastSeen. User-owned fields (socialTag, userNote) use last-write-wins via updatedAtMs timestamps. Near-simultaneous edits within a 5-second window are flagged as conflicts.

warning

Data persistence: Without Cloud Sync, your NodeDex is stored only in SQLite on-device. It survives app restarts but not app deletion. Uninstalling the app or switching phones permanently loses your local NodeDex. Cloud Sync or JSON export are your backup options.

import_export

Import & Export

NodeDex supports full JSON export and import with a sophisticated merge preview system. You can export your entire field journal as a JSON file and import it on another device — even without Cloud Sync.

Import Preview

Before applying an import, the system generates a complete ImportPreview that analyzes every entry against the local store without modifying any data. The preview reports:

Merge Strategies

Keep Local

Preserve local socialTag and userNote values when both exist

Prefer Import

Use imported values for conflicting user-owned fields

Review Conflicts

Per-entry overrides with ConflictResolution objects

Regardless of strategy, numeric fields always use max-wins and encounters/regions/co-seen edges are additively merged. Only user-owned fields (socialTag, userNote) are subject to conflict resolution.

military_tech

Explorer Titles

Explorer titles are prestige labels earned through actual mesh exploration — node count, region diversity, distance records, and encounter breadth. No grinding, no gamification. Just recognition of real activity. Titles are computed from NodeDexStats and displayed on the Album cover.

Newcomer
< 5 nodes

Beginning the mesh journey

Observer
5 – 19 nodes

Building awareness of the mesh

Explorer
20 – 49 nodes

Actively discovering the network

Cartographer
50 – 99 nodes

Mapping the invisible infrastructure

Signal Hunter
100 – 199 nodes

Seeking signals across the spectrum

Mesh Veteran
200+ nodes

Deep knowledge of the mesh

Mesh Cartographer
200+ nodes, 5+ regions

Charting regions and routes

Long-Range Record Holder
50+ nodes, 10km+ distance

Pushing the limits of range

phone_android

UI Screens & Providers

The NodeDex UI is built with Riverpod 3.x providers that compose the pure-function engines into reactive state. Each screen reads derived providers — no engine is ever called directly from widget code.

Main Screen

The NodeDexScreen presents a searchable, filterable, sortable list of all discovered nodes. Users can toggle between List mode (detailed tiles with metrics) and Album mode (collector card grid). Filter chips support trait-based and social-tag-based filtering. Sort options include last seen, first seen, encounter count, and distance.

Detail Screen

The NodeDexDetailScreen shows the full profile for a single node: animated sigil header, trait badge with evidence, field note, discovery stats, signal records, social tag editor, user note, region history, recent encounters timeline, co-seen links, and live device telemetry. UI elements are gated by the DisclosureState — sparse nodes show minimal information.

Constellation Screen

The ConstellationScreen wraps the ConstellationPainter in a gesture-aware viewport with pinch-to-zoom, pan, and tap-to-select. It includes a detail panel that slides up when a node or edge is selected, showing relationship details and navigation to the full profile.

Key Providers

  • nodeDexProvider — The main NodeDexNotifier managing all entries
  • nodeDexEntryProvider(nodeNum) — Single entry lookup
  • nodeDexTraitProvider(nodeNum) — Primary trait inference for a node
  • nodeDexScoredTraitsProvider(nodeNum) — Full ranked trait list with evidence
  • nodeDexPatinaProvider(nodeNum) — Patina score computation
  • nodeDexDisclosureProvider(nodeNum) — Disclosure tier and visibility flags
  • nodeDexFieldNoteProvider(nodeNum) — Deterministic field note text
  • nodeDexStatsProvider — Aggregate statistics and explorer title
  • nodeDexConstellationProvider — Full constellation graph data
  • nodeDexSortedEntriesProvider — Filtered, sorted list for the main screen
End of Document
info

This documentation covers the NodeDex system as of schema version 4. For user-facing help content, see the in-app guided tours available on each screen via the help button. For general Socialmesh documentation, visit the Documentation page.

Explore the Mesh with NodeDex

Download Socialmesh and start building your personal mesh field journal. Every node you discover earns a unique sigil, a personality trait, and a patina that deepens with real observation.