{/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */}
This is the orientation document: what lives where, how a change travels through the layers, and which document to read next. It assumes you have read Overview for the vocabulary and can build Personal AI Router (PAIR) by following Building and Running.
For contribution policy, refer to CONTRIBUTING.md. It covers branch and pull-request expectations, what to discuss first, and the testing and documentation requirements. This guide is about the code.
desktop/ The Electron + React application
services/ The Go services: broker, workers, proxies, terminal interface
services/ is the source of truth for runtime behavior. desktop/ is a client
of it. If you are changing what PAIR does, you are usually in services/. If
you are changing how a user sees or controls it, you are in desktop/.
A rule worth internalizing before your first change: the desktop application does not reimplement service behavior. It relays commands and renders reported state. If you find yourself writing routing, scheduling, discovery, or cryptography in TypeScript, the change belongs in a Go service instead.
| Path | What it owns |
|---|---|
src/ui/ |
The React renderer: components, stores, and the renderer-side API |
src/ui/stores/ |
One store per domain — nodes, engines, models, workloads, errors, and metrics |
src/ui/api/pair-api.ts |
The renderer's view of the service surface |
src/preload/ |
The typed bridge exposing window.pairApi and window.windowApi |
src/electron/ |
Main process: broker supervision, service bridge, and interprocess communication (IPC) |
src/electron/service-bridge/ |
Dispatches service calls and projects backend state |
src/electron/ipc/ |
Electron-native operations (windows, tray, clipboard, and logs) |
src/shared/ |
Types, constants, and utilities shared by all of the above |
scripts/ |
Build, license, and contract tooling |
tests/ |
Vitest unit tests |
The files you touch most often:
| File | Why |
|---|---|
src/shared/types/ws-channels.ts |
Every logical service channel: WsInvokeChannelMap for requests, WsPushChannelMap for pushes |
src/shared/types/ipc-channels.ts |
IpcChannelMap, the contract for Electron-native IPC |
src/electron/service-bridge/empty-handlers.ts |
handleServiceBridgeInvoke, the single dispatch every service call goes through |
src/electron/service-bridge/modular-state.ts |
ModularBridgeState — backend notifications become renderer push events here |
src/electron/service-bridge/modular-supervisor.ts |
The broker process and its JSON-RPC link |
src/shared/constants/modular-binaries.ts |
The canonical inventory of shipped binaries and who launches them |
src/shared/constants/modular-runtime.ts |
Backend-coupled constants — the only place a port or path literal belongs |
Each component is its own Go module in its own directory, with its tests beside
its source and a README.md describing its JSON-RPC surface. Shared code lives
in shared/, and tests/ holds cross-process tests that drive real binaries.
Start with services/readme.md for the component
inventory, then the component's own README. The broker's
nvpair-ui-broker/README.md is the most
useful single document, because every client talks to it.
Most changes take one of a few shapes. These are the paths through the layers.
A capability starts in Go and ends in the interface. Skipping a layer causes drift:
- Implement it in the owning Go component, with tests beside the source.
- Expose it over that component's JSON-RPC surface and update its README.
- Relay it in
nvpair-ui-brokerif a client needs to reach it. - Add the channel to
WsInvokeChannelMaporWsPushChannelMap. - Handle it in
handleServiceBridgeInvoke, and project any notification throughModularBridgeState. - Consume it in the owning renderer store, and render from that store.
- Run
npm run service-contracts:checkand update the affected docs.
If the capability matters on a headless machine, add it to the terminal interface
under services/nvpair-tui/ as well. That is a separate client with its own
views, so nothing reaches it automatically.
Update the producer, the broker relay, every consumer, the tests, and the documentation in the same change. Then regenerate and verify:
cd desktop
npm run service-contracts:write # regenerate docs/services-api.md
npm run service-contracts:check # fails on drift or stale generated outputHandling a method is not the same as covering it. Check the Go struct's JSON fields and confirm each one you care about reaches the consumer.
The local wire format is JSON-RPC 2.0 with relay protocol v2. Every notification
carries epoch and seq; requests and responses carry neither. broker:resync
returns the epoch and protocol: 2. Go codecs treat envelope mismatches as
terminal, not as recoverable malformed JSON. Keep matched clients and workers.
On an epoch change, the desktop bridge rehydrates each domain from authoritative
snapshots (discovery, membership, engines/models, errors, workloads, proxy
status, and workers:get-status). Sequence gaps do not trigger snapshot polling.
The broker sends a heartbeat every 15 seconds; desktop owns bounded stop/start recovery after
45 seconds of inbound silence, followed by readiness and snapshot recovery.
This does not imply unconditional reconnect after an unexpected broker exit or
heartbeat monitoring of individual workers.
Worker health is a separate broker-owned layer: each worker answers
worker/health; the broker probes every 30 s with a 5 s deadline, marks
degraded after two failures and restarts only eligible workers after three.
--worker-health-interval has a 1 s minimum; the deadline is capped at the
interval. Optional initial-spawn retries, crashes and eligible hangs share the
1 s → 16 s / five-replacement budget; healthy reset needs 60 s uptime and a
successful probe since spawn. Missing defaults wait for workers:restart.
workers:update is an unconditional full snapshot; desktop and TUI recover
workers:get-status after epoch changes, render the reported state, and relay
workers:restart without implementing policy. Settings → Service → Workers
and TUI Overview (R, with confirmation) expose the controls. Engine-manager
and the three proxies never auto-restart on a hang; see
Health and Failure Detection
for the eight eligible workers and the Unix-only cross-process test boundary.
State flows one way: the service is authoritative, stores hold snapshots, and push events update them.
- Fetch an initial snapshot, then subscribe to that domain's pushes.
- Treat commands as fire-and-forget. Do not populate durable state from a mutation's response.
- Do not add per-component loading flags for engine or model operations.
src/ui/stores/pending-actions.store.tsis the one sanctioned optimistic store, and backend state always supersedes it.
Window, tray, clipboard, log, and updater operations are IPC, not service calls.
Add the channel to IpcChannelMap, then implement it with safeHandle() so it
returns the typed result envelope instead of throwing across the boundary.
These are not style preferences. Reviews and tooling catch them.
- No type casting. No
as Type,as any,as unknown, or: any, and nounknownin signatures. If a cast seems necessary, the types are wrong. - Absolute imports across directories. Use the
@/...alias for anything that would need../. Within one directory,./siblingis correct. - Static imports only. No
await import(). - No renderer imports from
src/electron/. If a renderer file needs something from the main process, it belongs insrc/shared/or behind the preload bridge. - Say engine, not backend, in user-facing copy and new code. Existing wire names and Go symbols keep their spelling, because they are external contracts.
- No legacy fallbacks. One canonical path. Delete what you replace rather than leaving a compatibility branch.
- Keep prompts, messages, response bodies, PINs, and key material out of
system logs. Never log inference content in system log sinks (
slog,applog, stderr, file logs). Spans may optionally capture prompt and completion payloads directly to the local OpenTelemetry collector ONLY when explicitly enabled by the user for local AI metrics and evaluation. Pairing PINs, auth keys, and credentials remain strictly forbidden in all sinks at all times.
The cluster manager returns a pairing PIN in protocol results and notifications
on the broker's stdout; protocol output is not itself a system log.
src/electron/service-bridge/json-rpc-subprocess.ts sanitizes it with
redactSensitiveLogText from src/electron/redact-log.ts before emitting the
mirrored desktop log copies, while parsing the original frame for the UI.
Preserve that boundary for every new log or export path; it is PIN redaction,
not a general-purpose secret scrubber.
Local-only evaluation payload capture requires explicit user consent, enforces loopback-only exporter transport, truncates payloads to 16 KB, allowlists headers, and records no spans on cluster serving nodes. Proxies start with tracing disabled and are configured by the broker from saved settings rather than ambient environment variables; see Observability and SECURITY.md.
Desktop persists modularLogLevel, defaulting to warn, and passes it to the
broker and workers. Current unreleased logging changes preserve structured
service severity and write verbose protocol/debug entries to the desktop log
file only at debug. Standalone Go services still default to info.
Do not infer severity solely from stderr or promise that idle operation has no
I/O.
On Linux and macOS, the Makefile at the repository root wraps everything, and
it is the shortest path to a clean run:
make check # SPDX headers, build-script verify, lint, typecheck, contracts, desktop tests
make test # desktop unit tests plus go test in every services moduleRun make on its own to list the targets. Run the underlying commands directly
on Windows, or when you are iterating on a single check. From desktop/:
npm run lint
npm run typecheck
npm run test:unit
npm run service-contracts:check
npm run dead-code:check
npm run licenses:checkdead-code:check is not included in make check; run it separately.
npm run licenses:check (also callable as make licenses-check from the repository root)
is a release check that verifies third-party notices against attribution-manifest.json
and fails on stale notices or unresolved entries without downloading Go modules; the
gate now passes cleanly across all dependencies.
Run the Go tests from a Go component directory, and from services/tests for
cross-process coverage:
go test ./...Use npm run typecheck rather than tsc directly, because the project has split
Node and web targets. Refer to
services/readme.md for which Go tests skip
themselves and why a skip is not a pass.
- Architecture — process model, node-to-node transport, discovery, routing, and trust boundaries
desktop/docs/architecture.md— the application's internals, anddesktop/README.mdindexes its other documentsdesktop/docs/services-api.md— the generated JSON-RPC surface, never edited by handdesktop/docs/services-backend.md— how desktop consumes the services and how to update that integration- CONTRIBUTING.md — policy, pull requests, and review
- SECURITY.md — what PAIR does and does not defend against