System Architecture

Nessie is built around three layers: the API layer (shared models), the platform layer (runtime & plugin orchestration), and the plugin layer (all features). Understanding this separation is key to extending the system.

High-Level Overview

WEB SERVER PLUGIN LAYER PLATFORM LAYER API LAYER (nessie-api) flask-web-nessie django-web-nessie datasource load_graph visualizer visualise_graph manipulation filter_graph nessie-web render PluginManager discover · register · dispatch WorkspaceManager tabs · graphs · filters Context plugin interface Graph / Node / Edge Plugin Action FilterExpression Context (protocol)

High-level layer diagram: Web Servers → Plugins → Platform → API

The Plugin System

At the heart of Nessie is the PluginManager. When the platform starts, it calls discover_plugins() which uses Python entry points (nessie_plugins group) to find and register all installed plugins automatically — no manual wiring needed.

Each plugin is a Python function decorated with @plugin("Plugin Name") that returns a dictionary:

@plugin("My Plugin")
def my_plugin():
    return {
        "handlers": {
            "load_graph": handle_load_graph,   # action_name: handler_fn
        },
        "requires": ["filter_graph"],            # actions this plugin needs
        "setup_requires": {
            "File Path": SetupRequirementType.STRING
        },
    }

The handlers dict maps action names to handler functions. When an action is dispatched, the PluginManager finds the appropriate plugin and calls its handler with (action, context).

Core Data Models (nessie-api)

The nessie-api package defines the shared models that all plugins and the platform rely on. These are the only imports your plugin needs.

Graph + name: str + graph_type: GraphType + nodes: list[Node] + edges: list[Edge] + add_node() + to_dict() Node + id: str + attributes: dict + add_attribute(attr) + get_attribute(name) + remove_attribute() Edge + id: str + source: Node + target: Node + attributes: dict + add_attribute(attr) Attribute + name: str + value: Any Action + name: str + payload: dict FilterExpression + attr_name: str + operator: FilterOperator + value: Any Plugin + name: str + provided_actions: list[str] has many

Core data models from nessie-api

Request Lifecycle

When a user performs an action in the browser (e.g., opens a workspace), here is the full flow:

Browser HTTP Server Platform Datasource Plugin Web UI Plugin POST /perform-action perform_action() dispatch load_graph handle(action, ctx) return Graph context.add_workspace() dispatch render handle(action, ctx) return HTML string return HTML 200 OK (HTML) reload

Action request lifecycle: Browser → Server → Platform → Plugin → UI Render

Context Protocol

Plugins never access the platform directly. Instead, every handler receives a Context object that exposes a controlled interface for reading and writing shared state:

Method GroupKey MethodsDescription
Workspacesget_workspace_count(), add_workspace(graph), close_workspace_at(i)Manage workspace tabs
Graphsget_graph_at(i), set_graph_at(i, g), get_full_graph_at(i)Read/write workspace graphs
Filtersadd_filter_at(i, expr), clear_filters_at(i)Manage active filters
Consoleadd_console_message_at(i, msg), clear_console_messages_at(i)Log messages to the UI console
Searchget_search_at(i), set_search_at(i, q)Read/write search query state
Actionsperform_action(action, plugin_name?)Dispatch additional actions from within a handler
Visualizationget_visualised_graph_at(i), set_visualiser_at(i, name)Render graph HTML or switch active visualizer

Plugin Discovery

Nessie uses Python's entry points mechanism. Any installed Python package that declares a nessie_plugins entry point is automatically discovered when the platform starts.

# In pyproject.toml
[project.entry-points."nessie_plugins"]
my_plugin = "my_package:my_plugin_function"
Plugin Priority
When multiple plugins handle the same action, the first registered plugin is used by default. You can install a PluginPrioritization plugin to implement custom selection logic.