Cosmosh Architecture
1. Runtime Topology
Cosmosh uses an Electron dual-process model with an embedded backend service:
- Main Process (
packages/main/src/index.ts): app lifecycle, BrowserWindow creation, preload wiring, IPC registration, backend process orchestration. - Preload Bridge (
packages/main/src/preload.ts): strict API surface exposed viacontextBridge. - Renderer Process (
packages/renderer/src): React UI, xterm UI, state orchestration. - Backend Process (
packages/backend/src/index.ts): Hono HTTP API + WebSocket session services for SSH and local terminal.
flowchart LR R[Renderer React App] -->|window.electron.*| P[Preload Bridge] P -->|ipcRenderer.invoke/send| M[Electron Main] M -->|HTTP localhost + internal token| B[Backend Hono API] R -->|WebSocket token URL| WS1[SSH WS Service] R -->|WebSocket token URL| WS2[Local Terminal WS Service] B --> WS1 B --> WS2 B --> DB[(SQLite via Prisma)]
2. Main ↔ Renderer Responsibilities
Main Process (packages/main/src/index.ts)
- Starts BrowserWindow and backend warmup in parallel during app bootstrap.
- Keeps a single in-flight backend startup promise to deduplicate concurrent startup triggers.
- Main-process backend proxy requests now ensure backend readiness before forwarding HTTP calls.
- In development startup, main uses an incremental preflight (
packages/main/scripts/dev-preflight.cjs) and skips@cosmosh/api-contract/@cosmosh/i18nrebuilds when outputs are fresh. - Main launches backend with a runtime-only non-watch command (
dev:runtime) to avoid duplicatepredevrebuilds and reduce sustained CPU noise on laptops. - Owns app-level capabilities: locale persistence (in-memory), window/devtools/file-manager actions.
- Proxies renderer requests to backend endpoints with:
COSMOSH_INTERNAL_TOKENas internal auth header.- locale header for i18n-compatible backend responses.
Backend Process (packages/backend/src/index.ts)
- Registers idempotent graceful-shutdown flow for runtime signals and fatal process events.
- Shutdown order is explicit: stop WS session services, close HTTP listener, then disconnect Prisma/SQLite handles.
- Windows-specific termination (
SIGBREAK) is handled in the same path as POSIX signals to reduce stale DB lock cases. - Local terminal profile discovery now uses short-lived in-memory caching and parallel probing, reducing repeated profile scan latency on Home/Settings first-load paths.
- Startup includes idempotent Prisma migration-file execution in
initializeDatabase(...), so first install launch and every subsequent launch both converge local DB structure to the current backend schema contract before serving HTTP routes. - Schema sync is fail-fast: backend startup stops when required tables still cannot be reconciled after runtime migration execution, preventing partial/undefined API behavior.
- Migration ledger metadata is stored in Prisma-compatible
_prisma_migrationsformat to keep a future path open for nativeprisma migrate deploy/resolveworkflows.
Renderer Process (packages/renderer/src)
- Uses
window.electronbridge only (no direct Node API usage). - Creates SSH/local terminal sessions through backend APIs.
- Connects terminal data channels through WebSocket and renders with
xterm.js. - Non-home renderer pages (including SSH and settings editor/Monaco) are lazy-loaded to keep heavyweight assets out of the default startup path.
- Renderer bootstrap hydrates settings from local cache first, then refreshes canonical values from backend in background.
- Development StrictMode is opt-in via
VITE_ENABLE_STRICT_MODE=trueto reduce duplicate effect execution during local performance profiling. - SSH page uses tab-scoped connection intent snapshots (no global mutable target singleton), so retry/split flows are isolated per tab.
- Hidden tabs are rendered but cannot start new SSH connect side effects; connect flow is active-tab gated.
3. IPC Lifecycle (Current)
sequenceDiagram
participant UI as Renderer UI
participant PB as Preload Bridge
participant MP as Main Process
participant BE as Backend API
participant WS as WS Session Service
UI->>PB: window.electron.backendSshCreateSession(payload)
PB->>MP: ipcRenderer.invoke('backend:ssh-create-session', payload)
MP->>BE: POST /api/v1/ssh/sessions (+internal token)
BE-->>MP: sessionId + websocketUrl + websocketToken
MP-->>PB: API payload
PB-->>UI: API payload
UI->>WS: WebSocket connect (url + token)
WS-->>UI: { type: 'ready' }
UI->>WS: { type: 'input' | 'resize' | 'ping' }
WS-->>UI: { type: 'output' | 'telemetry' | 'pong' | 'exit' }
UI->>PB: close session
PB->>MP: ipcRenderer.invoke('backend:ssh-close-session', sessionId)
MP->>BE: DELETE /api/v1/ssh/sessions/{sessionId}
4. Security Model
Electron Surface Hardening
nodeIntegration: falsecontextIsolation: true- Renderer gets only explicit bridge APIs via
contextBridge.exposeInMainWorld. - Internal privileged operations stay in Main/Backend process.
Backend Access Boundary
- Backend is localhost-only and guarded by an internal runtime token (
COSMOSH_INTERNAL_TOKEN) in electron-main mode. - Main process injects headers and never exposes internal token to renderer.
- Credential encryption key is derived from
COSMOSH_SECRET_KEY/internal token hash in backend bootstrap. - HTTP i18n is request-scoped: backend middleware resolves locale from
x-cosmosh-locale(fallbackaccept-language), then injects a per-request translator used by all route response messages. - WS runtime i18n is session-scoped: session creation carries resolved locale into SSH/local terminal runtime so WS
error/exitmessages and close reasons are localized consistently. - i18n runtime is resource-injected: consumers register locale JSON payloads during
createI18n(...)setup, so each process bundles only its required scope data.
Session Channel Hardening
- WebSocket path includes sessionId and query token.
- Token mismatch or stale session causes immediate close (
1008). - Session attach timeout is enforced (30 seconds) to avoid orphaned resources.
5. Current Gaps / Planned Work
- SFTP runtime channel is not implemented yet; only SSH terminal and local terminal session channels are active.
- Renderer has SFTP entry placeholders in Home context menu; actual SFTP page/session wiring remains planned.
5.1 Settings Runtime (Implemented)
- Settings are now persisted by backend route
GET/PUT /api/v1/settings. - Storage model is a single-row JSON payload per scope (
scopeAccountId+scopeDeviceId) inAppSettings. - Scope defaults to local device (
deviceId=local-device) while keeping account scope field for future sync. - Renderer bootstrap (
packages/renderer/src/main.tsx) applies persisted language/theme using cached settings at startup, then synchronizes with backend. - Non-visual settings (for example SSH runtime limits) are persisted and discoverable, but some are intentionally not bound to runtime behavior yet.
- All setting definitions (types, defaults, constraints, enum sets, UI metadata, categories) live in a single registry:
packages/api-contract/src/settings-registry.ts. Adding or removing a setting only requires editing this file (plus i18n locale files). - Validation logic in
packages/api-contract/src/settings.tsis now generic and registry-driven: per-key rules (type check, enum, range, maxLength) are derived from the registry at runtime — no manual switch/case per key. - The OpenAPI
SettingsValuesschema is intentionally loose (type: object); strict TypeScript types and constraints live exclusively in the code registry. - Settings API response types (
ApiSettingsGetResponse,ApiSettingsUpdateResponse) are hand-crafted inpackages/api-contract/src/index.tsusing the strictSettingsValuesfrom the registry rather than generated from OpenAPI. - Stored settings payload parsing is forward-compatible: missing/new fields are backfilled per-field from defaults instead of resetting the entire settings object.
- Strict full-schema validation is still enforced for update requests (
PUT /api/v1/settings) to keep persisted payload shape deterministic.
sequenceDiagram
participant UI as Renderer
participant PB as Preload
participant MP as Main IPC
participant BE as Backend Settings Route
participant DB as SQLite(AppSettings)
UI->>PB: window.electron.backendSettingsGet()
PB->>MP: ipcRenderer.invoke('backend:settings-get')
MP->>BE: GET /api/v1/settings
BE->>DB: load AppSettings row by scope
DB-->>BE: payloadJson + revision
BE-->>MP: SettingsGetSuccess
MP-->>UI: settings payload
UI->>UI: apply language + theme
6. Core Data-Flow Views
6.1 Session Bootstrap Data Flow
flowchart TD UI[Renderer UI] --> BRIDGE[window.electron bridge] BRIDGE --> MAIN[ipcMain handler] MAIN --> API[Backend route] API --> SERVICE[Session service] SERVICE --> DB[(Prisma / SQLite)] SERVICE --> REMOTE[SSH host or local PTY] SERVICE --> TOKEN[WS token + session registry] TOKEN --> UI
6.2 Runtime Stream Data Flow
flowchart LR XT[xterm.js] --> IN[input events] IN --> WS[WebSocket] WS --> SVC[Backend session runtime] SVC --> REM[Remote shell / PTY] REM --> OUT[stdout + stderr] OUT --> WS2[WebSocket output events] WS2 --> XT2[xterm.js write]
6.3 Failure Boundary Model
- Renderer boundary: visual state and user interaction; failures should stay recoverable via UI retry.
7. SSH Keychain Credential Model (2026-03)
- SSH credentials are now persisted in
SshKeychainand linked fromSshServer.keychainId. SshServerkeeps connection identity and host policy (host,port,username,strictHostKey) but no longer stores encrypted password/private-key fields directly.- Keychain organization metadata reuses the same
SshFolderandSshTagdomains used by servers (no separate keychain-only folder/tag tables). - Existing per-server edit UX is preserved by allowing inline credential input in the SSH editor; backend transparently materializes/updates hidden keychains.
- Shared keychains can be reused by multiple servers; hidden keychains are intended for single-server private use.
- SSH session creation resolves credentials through server → keychain relation before opening
ssh2connections. - Main boundary: capability routing and internal auth injection; failures should never leak privileged tokens.
- Backend boundary: protocol validation, session lifecycle, and resource cleanup ownership.
- Remote boundary: SSH host / local shell instability is treated as external and mapped to stable UI error codes.
7. Architecture Decision Rationale
- Keep the backend as a separate runtime process to isolate protocol and credential handling from renderer attack surface.
- Use preload as a minimal bridge to reduce API exposure and preserve strict process contracts.
- Prefer WS data plane for terminal streams to avoid IPC bottlenecks on high-frequency I/O.
- Keep main as orchestrator/proxy instead of business-logic host for easier future server-client decoupling.
8. Boundary Case Playbook
8.1 Backend Not Ready at Startup
sequenceDiagram participant MAIN as Main Process participant BE as Backend Process participant UI as Renderer Window MAIN->>BE: start backend runtime MAIN->>UI: create BrowserWindow in parallel MAIN->>BE: poll /health BE-->>MAIN: not ready MAIN->>MAIN: retry with bounded wait BE-->>MAIN: healthy UI->>MAIN: first backend IPC request MAIN->>BE: await startup promise if needed
Handling principle:
- UI should become visible as early as possible while backend continues warming in parallel.
- First backend-bound IPC request must still observe backend ready-state before forwarding.
- Startup failure paths should be explicit and observable.
8.2 WS Attach Token Mismatch
sequenceDiagram
participant UI as Renderer
participant WS as Backend WS Gateway
UI->>WS: connect /ws/ssh/{sessionId}?token=invalid
WS-->>UI: close code 1008
UI->>UI: transition to failed state
UI->>UI: allow explicit retry flow
Handling principle:
- Token/session mismatch is security-sensitive and must fail closed.
- Recovery should create a fresh session/token path.
8.3 Renderer Reload During Active Session
sequenceDiagram participant UI1 as Renderer Instance A participant WS as Backend Session Runtime participant UI2 as Renderer Instance B UI1->>WS: active attach UI1-->>UI1: renderer reload UI2->>WS: re-attach with new token/session flow WS-->>UI2: ready or reject based on session state
Handling principle:
- Session runtime must guard against stale attach state.
- Renderer should treat reload as a new lifecycle and re-establish state explicitly.
8.4 Unreadable SQLite File During Startup
sequenceDiagram participant BE as Backend Bootstrap participant DB as SQLite File participant MAIN as Electron Main BE->>DB: Try SQLCipher bootstrap DB-->>BE: file is not a database / unreadable BE-->>MAIN: fail startup with DB_PRAGMA_FAILED
Handling principle:
- Production uses strict mode: SQLCipher/Prisma incompatibility fails fast and must be fixed operationally.
8.5 Startup Schema Upgrade Path
sequenceDiagram participant BE as Backend Bootstrap participant DB as SQLite BE->>DB: initializeDatabase(...) BE->>DB: apply PRAGMA + pending Prisma migration.sql files DB-->>BE: schema aligned (or error) BE->>BE: validate required table set BE-->>BE: continue startup only when validation passes
Handling principle:
- Runtime migration sync is idempotent and executes on every startup.
- Existing user data must remain intact while structural drift is repaired incrementally.