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.
Table of Contents
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.
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.
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 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
updatedAtMstimestamps for last-write-wins conflict resolution during sync
Rich History
-
encounters— Rolling window of the most recent 50EncounterRecords (timestamp, distance, SNR, RSSI, lat/lon) -
seenRegions— List ofSeenRegionrecords (regionId, label, firstSeen, lastSeen, encounterCount) -
coSeenNodes— Map ofCoSeenRelationshiprecords (count, firstSeen, lastSeen, messageCount) -
sigil— CachedSigilDatafor 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.
A known person you communicate with
Verified infrastructure relay or gateway
A node that reliably forwards traffic
Regularly co-seen on the mesh
Computed Properties
NodeDexEntry exposes several derived getters computed from the raw data:
-
isRecentlyDiscovered— True if first seen within the last 24 hours -
age/timeSinceLastSeen— Duration computations -
regionCount/coSeenCount/distinctPositionCount— Aggregate counts -
topCoSeenWeight— The strongest co-seen relationship count -
hasEnoughDataForTrait— Whether trait inference is meaningful
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
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.
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.
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
High throughput, forwards traffic
Role is ROUTER, ROUTER_CLIENT, REPEATER, or ROUTER_LATE with elevated channel utilization
Seen across multiple locations
3+ distinct positions or 2+ regulatory regions
Fixed position, long-lived guardian
10+ encounters over 3+ days from a stable position
Always active, high availability
Encounter rate exceeding 8 sightings per day
Rarely seen, elusive presence
Encounter rate below 0.3 per day relative to age
Carries messages across the mesh
High message-to-encounter ratio indicates deliberate data transport
Persistent hub with many connections
High co-seen density, fixed position, central to local topology
Irregular timing, fades in and out
Present but unpredictable, intervals vary widely with no periodicity
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:
-
infer()— Returns a primary trait + optional secondary (legacy API) -
inferAll()— Returns the full ranked list ofScoredTraitobjects, each carrying human-readableTraitEvidenceobservations
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.
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:
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
- Sigil visible
- Name + hex ID
- Nothing else
- Primary trait badge
- Trait evidence lines
- Field note visible
- Full trait list
- Patina stamp
- Observation timeline
- Identity overlay (15%)
- 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.
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
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:
- Relay Mark — Small directional tick for router/relay nodes
- Wanderer Mark — Tiny arc segment for mobile nodes seen across regions
- Ghost Mark — Faint hollow dot for rarely-seen nodes
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.
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
- 3 columns on phones (< 600dp), 4 columns on tablets
- Card aspect ratio: 5:7 (portrait, matching SigilCard)
- 8dp spacing grid, 16dp horizontal padding
- Sticky group headers with collection counts
- 2 mystery slots appended per group
Rarity Tiers
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.
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
Vertical streaks tied to packet activity and node count. Cool blue-grey tones. Fall speed 120–280 px/s with slight horizontal drift.
Rising sparks tied to patina scores and relay contribution. Warm amber-orange with sinusoidal horizontal wander and glow pulsing.
Drifting fog blobs tied to sparse data regions (ghost/unknown nodes). Translucent grey with slow lateral movement.
Ambient twinkling background particles. Always gently present when enabled. Pale blue-white points with slow twinkle cycle.
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:
- Constellation screen: 1.0× (full effect)
- Map overlays: 0.3× (subtle, must not interfere with map readability)
- Node detail screen: 0.25× (very subtle background)
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
- 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
- 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
- node_num, region_key PK
- label TEXT
- first_seen_ms INTEGER
- last_seen_ms INTEGER
- count INTEGER
- a_node_num, b_node_num PK
- first_seen_ms INTEGER
- last_seen_ms INTEGER
- count INTEGER
- message_count INTEGER
- CHECK (a < b)
- id INTEGER PK AUTO
- entity_type TEXT
- entity_id TEXT
- op TEXT
- payload_json TEXT
- attempt_count INTEGER
- key TEXT PK
- value TEXT
Migration History
- v1: Initial schema — entries, encounters, regions, co-seen edges, sync tables
-
v2: Added
social_tag_updated_at_msanduser_note_updated_at_msfor Cloud Sync conflict resolution -
v3: Added
last_known_nameto cache display names when nodes go offline -
v4: Added
last_known_hardware,last_known_role,last_known_firmwarefor 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 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
- Local mutation writes to SQLite
-
An outbox record is enqueued in the
sync_outboxtable - The sync service drains the outbox when enabled and network is available
- Each outbox entry is pushed to Firestore with retry logic (max 5 attempts)
- Successfully synced entries are removed from the outbox
Pull Flow
- The service queries Firestore for documents updated after the last watermark
-
Watermarks are per-user
(
nodedex_last_pull_ms_<uid>) to prevent leaking between accounts - Pulled entries are merged into SQLite using the same merge semantics as local merge
- User-owned fields (socialTag, userNote) use last-write-wins with per-field timestamps
- 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.
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
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:
- New entries (not in local store)
- Merge candidates (existing entries with additional data)
- Field conflicts (socialTag or userNote differs between local and imported)
- New co-seen edges, encounter records, and regions per entry
Merge Strategies
Preserve local socialTag and userNote values when both exist
Use imported values for conflicting user-owned fields
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.
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.
Beginning the mesh journey
Building awareness of the mesh
Actively discovering the network
Mapping the invisible infrastructure
Seeking signals across the spectrum
Deep knowledge of the mesh
Charting regions and routes
Pushing the limits of range
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 mainNodeDexNotifiermanaging 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
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.
See It in Action
NodeDex transforms raw mesh telemetry into a living field journal — here is what the experience looks like.