Architecture documentation for the StateSet Desktop application.
┌──────────────────┐ ┌──────────────────┐
│ Desktop App │ │ StateSet API │
│ (Electron + │───────▶│ │
│ React) │◀───────│ │
└──────────────────┘ REST └──────────────────┘
+ SSE
The desktop application is a control plane UI for managing autonomous AI customer service agents. It communicates with the StateSet API over REST and receives real-time updates via Server-Sent Events (SSE).
- Owns native window lifecycle (
createWindow()), tray mode, and app-level events. - Enforces renderer security boundaries:
contextIsolation: truenodeIntegration: false- navigation and popup restrictions (
will-navigate,setWindowOpenHandler) - production CSP/security header enforcement
- Handles secure secret persistence through Electron
safeStorage. - Exposes IPC handlers for:
- auth key management
- approved key/value store operations
- OAuth flow launches
- notifications, updates, and window controls
- Exposes a constrained
window.electronAPIobject viacontextBridge. - Acts as the only renderer-accessible gateway for privileged operations.
- Prevents direct Node.js or arbitrary Electron API access from renderer code.
- Initializes React, React Query, error boundaries, telemetry, logging.
- Initializes auth + preferences stores on startup.
- Uses protected routing:
- unauthenticated users ->
/login - authenticated users -> app shell (
Layout) and feature routes
- unauthenticated users ->
- Hosts feature pages (
Dashboard,AgentConsole,Connections,Settings, etc.).
- Engine API provides auth, sessions, streaming tokens, webhooks, secrets.
- Sandbox API supports sandbox execution-related features where configured.
| Layer | Technology |
|---|---|
| Framework | Electron + React + Vite + TypeScript |
| State Management | Zustand + React Query |
| Styling | Tailwind CSS |
| API Client | Native Fetch with retry/circuit breaker |
| Testing | Vitest + Testing Library + Playwright |
| Validation | Zod (runtime schema validation) |
src/lib/api.ts— API client with retry, circuit breaker, deduplicationsrc/lib/schemas.ts— Zod schemas for API response validationsrc/stores/auth.ts— Authentication state managementsrc/hooks/useAgentStream.ts— SSE streaming for real-time agent updatessrc/config/api.config.ts— API configurationelectron/main.ts— Electron main process, secure credential storageelectron/preload.ts— Secure renderer bridge (window.electronAPI)src/hooks/useOfflineCache.ts+src/lib/cache.ts— IndexedDB offline cache flowsrc/hooks/useBackgroundAgents.ts— tray sync + background notifications
src/
├── components/ # Shared UI components
├── config/ # App configuration
├── features/ # Feature modules (agent-console, etc.)
│ └── <feature>/
│ ├── components/
│ ├── hooks/
│ ├── constants.ts
│ └── utils.ts
├── hooks/ # Shared React hooks
├── lib/ # Core utilities (API client, schemas, metrics)
├── pages/ # Top-level page components
├── stores/ # Zustand stores
└── types/ # TypeScript type definitions
- Electron app becomes ready (
app.whenReady()inelectron/main.ts). createWindow()builds secure BrowserWindow and loads dev server or packaged renderer.- Preload attaches
window.electronAPI. - Renderer boots (
src/main.tsx) and mountsApp. Appcalls:useAuthStore().initialize()usePreferencesStore().initialize()
- Auth initialization (
src/stores/auth.ts) attempts:- read stored API key via
electronAPI.auth.getApiKey() - validate key with
GET /api/v1/auth/me - restore tenant, brands, current brand, sandbox key
- read stored API key via
- If auth passes, protected routes render in
Layout; otherwise app redirects to/login.
- All network-backed entities (sessions, connections, webhooks, etc.) live in React Query caches.
- Query keys are centralized in
src/lib/queryKeys.ts. - Mutations invalidate/refetch affected keys after writes.
src/stores/auth.ts: credentials, tenant, brand selection, auth lifecycle.src/stores/preferences.ts: UX preferences and behavior settings.src/stores/ui.ts: command palette and lightweight global UI state.- Additional stores manage notifications/audit/template slices.
- Secure credentials: Electron
safeStorage(main process). - Non-sensitive preferences/state:
electron-storeallowlisted keys. - Offline entity cache: IndexedDB with TTL (
src/lib/cache.ts).
All REST calls run through apiRequest() in src/lib/api.ts.
Pipeline behavior:
- Build auth headers from active auth state.
- Enforce timeout and retry with exponential backoff for retryable failures.
- Apply circuit breaker protections to avoid repeated cascading failures.
- Deduplicate concurrent identical safe reads (
GET,HEAD,OPTIONS). - Validate response shape with Zod schemas.
- Record metrics and emit structured logging.
This is the end-to-end path for pressing Start on Dashboard and then observing live agent output.
- UI action occurs in
src/pages/Dashboard.tsx. - Start mutation uses optimistic state (
useOptimisticSessionMutation) so row status updates immediately. - Mutation calls
agentApi.startSession(tenantId, brandId, sessionId). agentApi.startSessionissuesPOST /api/v1/tenants/:tenantId/brands/:brandId/agents/:sessionId/start.apiRequest()applies timeout/retry/circuit-breaker + metrics instrumentation.- On success, React Query invalidates sessions and refetches canonical state.
- Session status transitions to running in refreshed query data.
- Agent Console (
src/pages/AgentConsole.tsx) usesuseAgentStream()to connect to SSE endpoint. - Stream events are parsed and normalized in
src/hooks/useAgentStream.ts. - Message timeline, metrics, logs, typing indicators, and status badges update in real time.
- Background management (
src/hooks/useBackgroundAgents.ts) updates tray counts and optional notifications.
Connectivity/health indicator flow (src/hooks/useOnlineStatus.ts):
- On interval, app checks browser online state (
navigator.onLine). - Performs
GET /healthwith request timeout to validate reachability and latency. - If authenticated, follows with
GET /health/detailedfor component-level status. - Updates UI health state:
- API reachable or not
- component statuses (
database,redis,nats) - server circuit-breaker states
- local circuit-breaker state from client API layer
- Applies adaptive retry interval with backoff when failures occur.
The desktop app communicates with the StateSet API. All requests go through the centralized apiRequest() client which provides:
- Automatic retries with exponential backoff
- Circuit breaker to prevent cascading failures
- Request deduplication for concurrent identical GET requests
- Timeout handling with configurable limits
- Performance metrics tracking
API endpoints are configured via environment variables:
# Primary API endpoint
VITE_API_URL=https://engine.stateset.cloud.stateset.app
# Sandbox API (optional, for code execution features)
VITE_SANDBOX_API_URL=https://api.sandbox.stateset.appSee .env.example for all available configuration options.
The app authenticates using API keys stored securely via Electron's safeStorage encryption:
Authorization: ApiKey sk-xxxxxxxxxxxxxxxxxxxxx
Agent activity is streamed to the desktop app via SSE:
type AgentEventType =
| 'status_changed' // Agent status updates
| 'thinking' // Agent reasoning
| 'message' // Chat messages
| 'tool_call' // Tool invocation
| 'tool_result' // Tool execution results
| 'error' // Error events
| 'metrics' // Performance metrics
| 'heartbeat'; // Keep-alive pingThis section documents the runtime contract between desktop and
stateset-orchestration-engine, including compatibility handling for known payload variants.
| Category | Desktop Route Usage | Engine Auth Requirement |
|---|---|---|
| Health | GET /health, GET /api/v1/health/detailed with fallback GET /health/detailed on 404 |
/health is public. /api/v1/health/detailed requires API auth scopes (read/admin/ops/metrics). |
| Agents | GET/POST/PUT/DELETE under /api/v1/tenants/:tenant/brands/:brand/agents/* and /api/v1/tenants/:tenant/agents |
API auth with tenant + brand authorization checks; write scope required for mutations. |
| Streaming | POST /stream/token, GET /stream (SSE) |
Protected token route + callback auth middleware for SSE stream route. |
| Secrets | GET/POST/DELETE and POST /test under brand secrets endpoints with tenant/brand fallbacks |
API auth with read/write scope and tenant-brand ownership checks. |
| Webhooks | Tenant-scoped list/get/update/delete/test/deliveries plus brand-scoped create | API auth with read/write scope and tenant-brand ownership checks. |
| Auth | POST /auth/register, POST /auth/login, GET /auth/me |
Register/login are public; /auth/me is protected. |
Desktop currently normalizes these response variants:
{ ok, data: T }envelope viaunwrapDataEnvelope(...)- Direct payloads like
{ ok, webhooks: [...] }or{ success, ... } - Secrets list variants:
{ ok, platforms: string[] }{ ok, data: { platforms: string[] } }
- Webhook delivery variants:
- Legacy shape with
payload: string,duration_ms,success - Canonical engine DB shape with
payload: object,response_status,attempts,delivered_at
- Legacy shape with
- Webhook test variants:
- Legacy
{ success, status_code, duration_ms } - Canonical
{ ok, data: WebhookDelivery }normalized to the legacy shape for UI callers
- Legacy
- Webhook create variants:
- Full webhook object
- Minimal create payload (
id/url/events/secret/enabled/created_at) normalized with tenant/brand/name fallbacks
- Desktop always checks
GET /healthfirst for reachability. - For detailed status, desktop prefers
GET /api/v1/health/detailed. - If that route returns
404, desktop falls back toGET /health/detailed. - Detailed payload parsing is defensive:
- supports top-level and
{ ok, data }envelope shapes - normalizes unknown component statuses to
unknown(not falselyunhealthy) - defaults missing circuit breaker fields to
closed
- supports top-level and
npm test -- src/lib/api.test.ts src/lib/schemas.test.ts src/hooks/useOnlineStatus.test.ts src/lib/registration.test.ts
npx eslint src/lib/api.ts src/lib/schemas.ts src/hooks/useOnlineStatus.ts src/lib/registration.tsFor cross-repo checks against the engine implementation, see docs/ENGINE_API_ALIGNMENT.md.
- API keys encrypted at rest using Electron
safeStorage - Sensitive data sanitized in error reporting
- Content Security Policy enforced in renderer
- OAuth credentials loaded from environment variables (never hardcoded)
- Renderer cannot access Node directly; all privileged access goes through preload IPC bridge
# Install dependencies
npm install
# Start in development mode
npm run dev
# Run tests
npm test
# Build for production
npm run build