Dashboard Data Architecture
How the dashboard fetches data from provider plugins using GraphQL-over-event-bus contracts
Overview#
The Network Overview dashboard (/dashboard) displays real-time network metrics, protocol info, fees, pipeline usage, GPU capacity, pricing, and a live job feed. All data is provided by plugins — the core dashboard contains zero hardcoded data.
This architecture uses a GraphQL-over-event-bus pattern: the dashboard sends a GraphQL query string through the event bus, and whichever plugin has registered as the handler executes the query and returns the results.
Why This Design#
| Concern | Solution |
|---|---|
| Decoupling | Core dashboard imports nothing from any plugin. It only uses well-known event names from the SDK. |
| Flexibility | Any plugin can become the data provider — just register a handler. |
| Partial data | GraphQL returns null for unresolved fields. Widgets degrade gracefully. |
| Single request | One GraphQL query fetches all widget data in a single event bus round-trip. |
| Schema evolution | Add new fields without breaking existing queries. Deprecate old fields with @deprecated. |
| Type safety | The SDK exports TypeScript types that mirror the GraphQL schema. |
Architecture#
The system has three layers:
1. Plugin SDK (Shared Contracts)#
Located in packages/plugin-sdk/src/contracts/, this layer defines:
DASHBOARD_SCHEMA— the GraphQL SDL string that is the contractDASHBOARD_QUERY_EVENT— the well-known event name ('dashboard:query')DASHBOARD_JOB_FEED_EVENT— the job feed subscription eventDashboardResolvers— TypeScript interface for resolver functionscreateDashboardProvider()— helper that reduces plugin boilerplate to 3 lines
2. Core Dashboard (Consumer)#
Located in apps/web-next/src/app/(dashboard)/dashboard/page.tsx, the dashboard:
- Defines a GraphQL query string describing what data it needs
- Calls
useDashboardQuery()which sends the query viaeventBus.request() - Calls
useJobFeedStream()for real-time job events - Renders widgets from the response data
- Shows skeletons during loading, fallbacks when no provider exists
3. Provider Plugin (Any Plugin)#
Any plugin that wants to provide dashboard data:
- Imports
createDashboardProviderfrom@naap/plugin-sdk - Implements resolver functions for the fields it supports
- Registers on mount, cleans up on unmount
Data Flow: Request/Response#
- Plugin mounts — calls
createDashboardProvider(eventBus, resolvers), which registers ahandleRequesthandler for'dashboard:query' - Dashboard renders — calls
useDashboardQuery(NETWORK_OVERVIEW_QUERY) - Hook sends request —
eventBus.request('dashboard:query', { query, variables }) - Event bus routes to handler — the registered plugin receives the request
- Plugin executes GraphQL —
graphql({ schema, source: query, rootValue: resolvers }) - Result flows back —
{ data, errors }returned through the event bus - Dashboard renders widgets — each widget receives its typed data via props
Data Flow: Live Job Feed#
The job feed uses a two-phase pattern:
- Discovery — Dashboard calls
eventBus.request('dashboard:job-feed:subscribe')to get channel info - Streaming — Based on the response:
- Ably mode: Subscribe to the returned Ably channel (production)
- Event bus fallback: Listen for
'dashboard:job-feed:event'events (local dev)
The mock provider plugin uses the event bus fallback and emits simulated jobs every 3.5 seconds.
SOLID Principles#
- Single Responsibility: Each file has one job —
dashboard.tsdefines contracts,createDashboardProvider.tshandles wiring,useDashboardQuery.tshandles fetching, widget components handle rendering - Open/Closed: Adding a new widget = add a field to the schema + a resolver + a rendering component. No existing code changes.
- Liskov Substitution: Any plugin implementing the same schema can replace another. The mock plugin is a drop-in placeholder for a real provider.
- Interface Segregation: Query contract and stream contract are separate. A plugin can implement one or both.
- Dependency Inversion: Core depends on abstract contracts (schema + event names), never on concrete plugins.
Comparison with Alternatives#
| Approach | Pros | Cons |
|---|---|---|
| GraphQL-over-event-bus (current) | One query, typed contract, partial results, plugin-agnostic | Requires graphql package in provider |
| Per-widget events | Simple individual events | 6+ event names, 6+ types, no partial results, chatty |
| Direct REST calls | Familiar HTTP pattern | Plugin-name-dependent URLs, multiple round-trips |
| Full GraphQL server | Standard tooling (Apollo, etc.) | Heavy infrastructure, HTTP overhead, not needed for in-process communication |
Security#
- Events are team-scoped by default — the event bus automatically prefixes business events with the current team ID
- Sandboxed plugins run with restricted event bus access (plugin name prefix enforced)
- The
graphqlexecution is in-process — no network boundary to secure - The provider plugin's resolvers control what data is returned
BFF Facade Layer#
In addition to the GraphQL-over-event-bus pattern for plugin-provided data, the dashboard also uses a Backend for Frontend (BFF) pattern for server-side data aggregation.
Architecture#
The BFF layer lives at /api/v1/dashboard/* and is backed by a Data Facade (apps/web-next/src/lib/facade/index.ts) — a single entry point for all UI data needs.
Dashboard UI → /api/v1/dashboard/kpi → Facade → Resolver → NAAP API / SubgraphData Sources#
The facade resolves data from multiple upstream sources:
| Domain | Source | Resolver |
|---|---|---|
| KPI (sessions, minutes, models) | NAAP Metrics API | resolvers/kpi.ts |
| Pipeline usage | NAAP Metrics API | resolvers/pipelines.ts |
| Pipeline catalog | NAAP Metrics API | resolvers/pipeline-catalog.ts |
| Orchestrators | NAAP Metrics API | resolvers/orchestrators.ts |
| GPU capacity | NAAP Metrics API | resolvers/gpu-capacity.ts |
| Pricing | NAAP Metrics API | resolvers/pricing.ts |
| Protocol info | Livepeer Subgraph (The Graph) | resolvers/protocol.ts |
| Fees | Livepeer Subgraph (The Graph) | resolvers/fees.ts |
| Job feed | NAAP Metrics API | resolvers/job-feed.ts |
| Network models | NAAP Metrics API | resolvers/network-models.ts |
| Network capacity | NAAP Metrics API | resolvers/net-capacity.ts |
| Performance by model | NAAP Metrics API | resolvers/perf-by-model.ts |
Stub Mode#
Setting FACADE_USE_STUBS=true forces all facade functions to return hardcoded stub data. This is useful for local development or when upstream APIs are unavailable. When unset, implemented resolvers call live APIs while unimplemented resolvers return stub data silently.
BFF Routes#
| Route | Method | Description |
|---|---|---|
/api/v1/dashboard/kpi | GET | Key performance indicators |
/api/v1/dashboard/pipelines | GET | Pipeline usage data |
/api/v1/dashboard/pipeline-catalog | GET | Available pipelines |
/api/v1/dashboard/orchestrators | GET | Orchestrator status |
/api/v1/dashboard/protocol | GET | Protocol info (from subgraph) |
/api/v1/dashboard/fees | GET | Fee data (from subgraph) |
/api/v1/dashboard/gpu-capacity | GET | GPU capacity overview |
/api/v1/dashboard/pricing | GET | Pricing data |
/api/v1/dashboard/job-feed | GET | Recent job feed items |
/api/v1/dashboard/dashboards | GET/POST | User dashboard configurations |
/api/v1/dashboard/preferences | GET/PUT | Dashboard display preferences |
/api/v1/dashboard/embed | GET | Embeddable dashboard URLs |
/api/v1/dashboard/config | GET/PUT | Dashboard configuration |
Adding a New Data Domain#
- Add the function signature to
apps/web-next/src/lib/facade/index.ts - Add stub data in
stubs.ts - Add the real resolver in
resolvers/<domain>.ts - Wire the BFF route to call the facade function