{/* 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.
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 logs. Never log inference content. Log operational metadata such as engine, model, job ID, and node ID instead.
The pairing PIN is the one exception that needs explaining. The cluster manager
returns it in results and notifications that travel the broker's stdout, so it
does reach the log path. src/electron/service-bridge/json-rpc-subprocess.ts
strips it with redactSensitiveLogText from src/shared/utils/redact-log.ts
before any sink sees it. That choke point makes the guarantee hold, so a new path
from subprocess output to a log or export must go through it.
On Linux and macOS, the Makefile at the repository root wraps everything, and
it is the shortest path to a clean run:
make check # 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:checkRun 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