NodeDex System Architecture
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.
System Stats:
- 14 subsystems
- 9 personality traits
- 6 patina axes
- 5 disclosure tiers
- 4 atmosphere effects
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.
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 providerslib/features/nodedex/screens/— UI screens (list, detail, constellation)lib/features/nodedex/widgets/— Reusable widgets (SigilCard, TraitBadge, PatinaStamp, etc.)lib/features/nodedex/album/— Collector Album systemlib/features/nodedex/atmosphere/— Elemental Atmosphere particle systemlib/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 timestampsencounterCount— Total distinct encounter sessions (5-minute cooldown between counts)lastKnownName— Cached display name, persists even when node goes offlinelastKnownHardware/lastKnownRole/lastKnownFirmware— Cached device metadata
Signal & Distance Records:
maxDistanceSeen— Furthest recorded range in metersbestSnr/bestRssi— All-time best signal quality recordsmessageCount— 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.
- 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:
isRecentlyDiscovered— True if first seen within the last 24 hoursage/timeSinceLastSeen— Duration computationsregionCount/coSeenCount/distinctPositionCount— Aggregate countstopCoSeenWeight— The strongest co-seen relationship counthasEnoughDataForTrait— 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
- 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.
Palette: Sky, Purple, Orange, Emerald, Red, Amber, Cyan, Pink, Teal, Indigo, Lime, Lavender, Magenta, Green, Rose, Deep Sky.
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
-
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:
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:
- Trace (0–9)
- Faint (10–24)
- Noted (25–39)
- Logged (40–54)
- Inked (55–69)
- Etched (70–84)
- Archival (85–94)
- Canonical (95–100)
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.
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:
- 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
- Common (50% distribution)
- Uncommon (30%)
- Rare (15%)
- Epic (4%)
- Legendary (1%)
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.
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
- 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
nodedex_entries:
- node_num (INTEGER PK)
- first_seen_ms, last_seen_ms, encounter_count
- max_distance, best_snr, best_rssi, message_count
- social_tag, user_note, sigil_json
- last_known_name, last_known_hardware, last_known_role, last_known_firmware
- deleted (soft-delete flag)
nodedex_encounters:
- id (PK AUTO), node_num (FK)
- ts_ms, distance_m, snr, rssi, lat, lon, session_id
nodedex_seen_regions:
- node_num, region_key (composite PK)
- label, first_seen_ms, last_seen_ms, count
nodedex_coseen_edges:
- a_node_num, b_node_num (composite PK, CHECK (a < b))
- first_seen_ms, last_seen_ms, count, message_count
sync_outbox:
- id (PK AUTO), entity_type, entity_id, op, payload_json, attempt_count
sync_state:
- key (PK), value
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
- 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.
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.
Titles:
- 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
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.
Key Providers
nodeDexProvider— The mainNodeDexNotifiermanaging all entriesnodeDexEntryProvider(nodeNum)— Single entry lookupnodeDexTraitProvider(nodeNum)— Primary trait inference for a nodenodeDexScoredTraitsProvider(nodeNum)— Full ranked trait list with evidencenodeDexPatinaProvider(nodeNum)— Patina score computationnodeDexDisclosureProvider(nodeNum)— Disclosure tier and visibility flagsnodeDexFieldNoteProvider(nodeNum)— Deterministic field note textnodeDexStatsProvider— Aggregate statistics and explorer titlenodeDexConstellationProvider— Full constellation graph datanodeDexSortedEntriesProvider— 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.