Architecture
Overview
TapMap is a local-first network awareness application centered around an interactive world map.
The application periodically collects active network connections, enriches them with GeoIP information, and visualizes them as connection lines and map markers.
TapMap maintains two separate views of network activity:
- Session state used for real-time visualization and network inspection.
- Historical state used for insights and long-term analysis.
The map is the primary user experience. Additional functionality is exposed through panels and modal views rather than separate application pages.
Design Principles
Local-first
TapMap runs entirely on the local machine.
Network activity data is processed locally unless the user explicitly configures an external integration such as MQTT.
GeoIP lookups use local databases.
Awareness, not control
TapMap is an awareness tool.
It visualizes and analyzes network activity but does not block, filter, or modify traffic.
MVC-inspired architecture
TapMap follows an MVC-inspired architecture adapted for Dash and Plotly.
The architecture separates:
- Model: data collection and backend integration
- State: deterministic application logic
- UI: rendering and presentation
- Controller: callback orchestration and application flow
High-Level Architecture
app.py
(Controller)
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
model state ui
(facade) (logic) (rendering)
Model Layer
The model layer acts as a backend facade.
Platform-specific network collection, GeoIP enrichment, and snapshot construction are hidden behind a unified interface.
The rest of the application interacts with the model through:
Model.snapshot()
rather than platform-specific implementations.
Examples:
Model
│
▼
NetInfo
│
┌────────────┴────────────┐
│ │
▼ ▼
PsutilNetInfo LsofNetInfo
(Linux/Windows) (macOS)
Responsibilities include:
- Socket inspection
- Process discovery
- Public IP discovery
- GeoIP enrichment
- Application information and platform verification
- Snapshot creation
State Layer
The state layer contains deterministic application logic.
Examples include:
- Connection and Unmapped state (session-lifetime)
- Insights processing
- Significant Connections evaluation and history
- Menu state
- Modal state
- Keyboard handling
- Status calculations
The state layer contains no Dash rendering code.
UI Layer
The UI layer builds Dash and Plotly output.
Responsibilities include:
- Map rendering
- Layout construction
- Modal content
- Daily Activity Report rendering
- Formatting helpers
The UI layer contains no business logic.
GeoDB Subsystem
The GeoDB subsystem manages GeoIP database lifecycle and provider integrations.
The subsystem consists of:
- A UI integrated into the shared modal framework
- A service layer responsible for orchestration
- Provider-specific implementations for MaxMind and DB-IP
Responsibilities include:
- Database installation
- Database updates
- Validation
- Provider abstraction
- Status reporting
GeoDB is intentionally separated from the model layer.
The model layer consumes geolocation data, while GeoDB manages the databases and providers that make geolocation possible.
This separation isolates provider-specific concerns such as downloads, credentials, version checks, validation, and activation from network collection and snapshot generation.
Lifecycle, Tray, and Autostart
TapMap runs as a single desktop process with two threads: the main thread owns the tray icon and application lifecycle, and the local web server runs on one background thread. The tray/lifecycle code communicates with the rest of the application only through a narrow interface and never reaches into Model or state stores directly.
All shutdown paths (tray Quit, the web UI Exit, process signals, and Docker container termination) go through the same coordinated shutdown sequence. Closing the browser tab does not stop TapMap; the browser and the TapMap process have independent lifetimes.
If a tray backend is unavailable, TapMap continues running without one; the web UI remains the way to interact with and exit the application.
Responsibilities:
lifecycle.py— shutdown request handling, the tray run loop, and signal handlers.tray.py— tray icon and menu construction (Open TapMap / Quit TapMap).autostart/— per-platform login autostart integration.
Autostart
TapMap can register itself to launch automatically at login. Each platform's native mechanism is the sole source of truth for current autostart state; it is never duplicated into settings.json.
- Windows: a per-user Scheduled Task, managed through Task Scheduler COM.
- macOS:
SMAppService.mainApp. - Linux: an XDG autostart desktop entry.
A one-time marker records that initial autostart setup has been handled. It never represents current ON/OFF state.
Docker/headless runs share the same lifecycle and shutdown model but have no tray and no autostart integration; container restart behavior is left to the container orchestration.
Data Flow
Network activity enters the system through periodic snapshots.
Network Connections
│
▼
Model.snapshot()
│
▼
model_snapshot
The snapshot drives two independent data flows.
Session Flow
Used for real-time visualization.
model_snapshot
│
├──────────────────────┬───────────────────────┐
│ │ │
▼ ▼ ▼
ConnectionAnalyzer Open Ports LAN/LOCAL Services
│
┌────┴────┐
│ │
▼ ▼
mapped unmapped
│ │
▼ ▼
ConnectionState UnmappedState
│ │
▼ ▼
ui_view Unmapped Services
│
▼
Map
ConnectionAnalyzer classifies each PUBLIC connection as mapped (usable GeoIP) or unmapped, and routes it into ConnectionState or UnmappedState accordingly. ConnectionState and UnmappedState accumulate observations for the lifetime of the application session.
Open Ports and LAN/LOCAL Services are not accumulated. They are read directly from the current snapshot on each render and never pass through ConnectionAnalyzer or either session state store.
AppInfo Flow
Application identity information is collected as part of normal snapshot processing. Expensive platform verification runs independently in the background.
Model.snapshot()
│
├── Application identity
│
▼
model_snapshot
│
▼
ConnectionState / UnmappedState
Background verification
│
▼
AppInfo cache
│
▼
normal poll
│
▼
ConnectionState / UnmappedState / SignificantConnections
Pending verification does not block snapshot creation or map updates. Completed verification information is merged into ConnectionState, UnmappedState, and SignificantConnections independently during subsequent polling.
Historical Flow
Used for long-term analysis.
model_snapshot
│
▼
ConnectionAnalyzer
│
├── per PUBLIC connection (mapped and unmapped):
│ evaluate significance against SignificanceHistory
│ │
│ ▼
│ significant? ──▶ SignificantConnections ──▶ significant_connections.json
│ │
│ ▼
│ notification policy
│ │
│ ▼
│ dispatch_notification()
│ │
│ ▼
│ notification channels
│
▼ (after the loop; mapped PUBLIC connections only)
process_insights()
│
▼
insights
│
├── Insights Panel
├── Daily Activity Report
└── Insights Log
│
▼
insights.json
Significant Connections evaluation runs per connection, inside the same loop that classifies mapped/unmapped connections, for every PUBLIC connection regardless of mapped/unmapped status. Novelty is judged against SignificanceHistory, an in-memory structure seeded from the persisted Insights bitmasks at startup and updated as each connection is evaluated; SignificanceHistory itself is not directly persisted. Connections judged significant are appended to SignificantConnections, a bounded, persisted event log.
Insights is a separate, batch-updated structure: after the per-connection loop completes, process_insights() updates the rolling 30-day Insights history using only the mapped PUBLIC connections observed in that poll. Unmapped PUBLIC connections are evaluated for significance but do not contribute to Insights.
Historical state survives application restarts.
Notification handling is separate from Significant Connections history. A newly accepted Significant Connection is stored first, then evaluated against the notification policy. Eligible events are passed to dispatch_notification(), which sends them independently to the configured notification channels. Significant Connections history is not a notification queue, and notification failures do not affect event storage or other channels.
MqttChannel owns the MQTT client and its network lifecycle, including connection and reconnection. DesktopNotificationChannel delegates notification delivery to the operating system on desktop installations. The channels operate independently and do not modify ConnectionAnalyzer, SignificantConnections, Insights, or UI state.
State Stores
model_snapshot
Represents the current network snapshot.
This state is transient and replaced during each polling cycle.
ConnectionState
Session-scoped state.
Accumulates mapped PUBLIC connections (usable GeoIP) observed during the current application session, routed here by ConnectionAnalyzer.
Retained entries may be enriched with completed AppInfo verification results during subsequent polling, even when the corresponding connection is no longer present in the latest snapshot.
Used by the Interactive Map.
Cleared when the application exits or the user clears the cache.
UnmappedState
Session-scoped state.
Accumulates PUBLIC connections without usable GeoIP observed during the current application session, routed here by ConnectionAnalyzer.
Retained entries may be enriched with completed AppInfo verification results during subsequent polling, the same as ConnectionState.
Used by Unmapped Services.
Cleared when the application exits or the user clears the cache.
SignificanceHistory
Runtime-only novelty tracking. Not directly persisted.
Records the last-seen day for each observed application, country, provider, and port, plus verification-failure history. The last-seen dictionaries are seeded once from the persisted Insights bitmasks at startup; verification-failure history is shared with InsightsState. The history is then updated in memory as each PUBLIC connection is evaluated.
Used by ConnectionAnalyzer to decide whether a connection is significant.
SignificantConnections
Persistent historical state.
A bounded, chronological event log (most recent 500) of PUBLIC connections judged significant against SignificanceHistory, covering both mapped and unmapped connections.
Completed AppInfo verification results may update retained events during subsequent polling.
Loaded at startup and periodically written to disk as significant_connections.json.
insights
Persistent historical state.
Maintains rolling 30-day activity history for:
- Applications
- Countries
- Providers (ASN)
- Ports
Populated only from mapped PUBLIC connections; unmapped PUBLIC connections do not contribute.
Loaded at startup and periodically written to disk as insights.json.
Used by:
- Insights Panel
- Daily Activity Report
- Insights Log
Architectural Boundaries
The following boundaries should be preserved:
- model does not contain UI rendering
- state does not contain Dash or Plotly rendering
- ui does not contain business logic
- GeoIP database management remains isolated in geodb
- historical state remains separate from session state
- tray/lifecycle code does not directly access or manipulate model/state-owned application state
- notification channels do not modify analyzer, historical, or UI state
Repository Structure
src/tapmap/
├── app.py Controller and callback orchestration
├── config.py Application configuration
├── runtime.py Runtime initialization
├── lifecycle.py Shutdown coordination and the tray run loop
├── tray.py System tray icon and menu
├── mqtt_cli.py MQTT configuration CLI
├── mqtt_config.py MQTT configuration and persistence
│
├── model/ Network collection and GeoIP enrichment
├── state/ Application state and decision logic
├── ui/ Dash and Plotly rendering
├── geodb/ GeoIP database management
├── notifications/ Notification channels and dispatch
├── autostart/ Per-platform login autostart integration
│
└── assets/ Static Dash assets
Startup Flow
Application startup begins in:
tapmap.__main__
↓
main()
↓
build_runtime()
↓
TapMap()
↓
run()
During startup:
- Runtime configuration is created.
- GeoIP services are initialized.
- Historical insights are loaded from disk.
- Dash callbacks are registered.
- The tray and browser launch are initialized, the web server starts on a background thread, and the main thread runs the application lifecycle.