Architecture
Architecture overview for DorkOS contributors
Architecture
DorkOS uses a hexagonal (ports & adapters) architecture that lets the same React client run in two modes:
- Standalone web — Express server with HTTP/SSE communication
- Obsidian plugin — In-process services, no server or network required
This guide explains how the pieces fit together.
Monorepo Structure
DorkOS is a Turborepo monorepo with five apps and nine shared packages:
The Transport Interface
The core abstraction is the Transport interface (packages/shared/src/transport.ts). It defines all client-server communication — session management, messaging, tool interaction, Tasks, Relay, Mesh, and agent identity.
A condensed view of the core methods:
interface Transport {
createSession(opts): Promise<Session>
listSessions(cwd?) → Promise<Session[]>
getSession(id, cwd?) → Promise<Session>
getMessages(sessionId) → Promise<{ messages: HistoryMessage[] }>
sendMessage(id, content, onEvent, signal?, cwd?, options?) → Promise<void>
// options: { clientMessageId?: string; uiState?: UiState }
stopTask(sessionId, taskId) → Promise<{ success: boolean; taskId: string }>
approveTool(sessionId, toolCallId) → Promise<{ ok: boolean }>
denyTool(sessionId, toolCallId) → Promise<{ ok: boolean }>
getCommands(refresh?) → Promise<CommandRegistry>
getModels() → Promise<ModelOption[]>
getCapabilities() → Promise<{ capabilities, defaultRuntime }>
health() → Promise<{ status, version, uptime }>
}The interface extends further to cover Tasks schedules, Relay messaging, Mesh agents, bindings, and admin operations. See packages/shared/src/transport.ts for the complete definition.
Two Transport Implementations
HttpTransport communicates with the Express server over HTTP:
- Uses
fetch()for CRUD operations - Parses Server-Sent Events (SSE) streams in
sendMessage() - Converts SSE events into
StreamEventobjects that update the UI
Location: apps/client/src/layers/shared/lib/http-transport.ts
DirectTransport calls service instances directly in the same process:
- No HTTP, no port binding, no network serialization
- Iterates
AsyncGenerator<StreamEvent>from the Claude Agent SDK - Lower latency, ideal for embedded contexts
Location: apps/client/src/layers/shared/lib/direct-transport.ts
Both implementations expose the same interface, so the React client doesn't know which one it's using.
Dependency Injection via React Context
The Transport is injected into the React app via a context provider:
// main.tsx
const transport = new HttpTransport({ baseUrl: '/api' })
<TransportProvider transport={transport}>
<App />
</TransportProvider>// CopilotView.tsx
const runtime = new ClaudeCodeRuntime(repoRoot)
const commandRegistry = new CommandRegistryService(repoRoot)
const transport = new DirectTransport({
runtime,
transcriptReader: runtime.getTranscriptReader(),
commandRegistry,
vaultRoot: repoRoot
})
<TransportProvider transport={transport}>
<ObsidianApp>
<App />
</ObsidianApp>
</TransportProvider>Components and hooks access the transport via useTransport():
import { useTransport } from '@/layers/shared/model/TransportContext';
function MyComponent() {
const transport = useTransport();
const sessions = await transport.listSessions();
}Server Architecture
The Express server (apps/server/) is organized into routes and services.
Routes
Routes are thin HTTP handlers. They obtain the active runtime via runtimeRegistry.getDefault():
Prop
Type
Services
Prop
Type
Session Storage: SDK JSONL Transcripts
Sessions are not stored in a database. TranscriptReader scans SDK JSONL files at ~/.claude/projects/\{slug\}/*.jsonl. All sessions are visible regardless of which client created them — CLI, DorkOS, or otherwise.
- Session ID = SDK session ID (UUID from filename)
- No delete endpoint — sessions persist in SDK storage
- Session metadata (title, preview, timestamps) extracted from file content on every request
ClaudeCodeRuntimecalls SDKquery()withresume: sessionIdfor continuity across clients
Client Architecture
The React client (apps/client/) uses Feature-Sliced Design (FSD):
FSD Layers
| Layer | Purpose | Examples |
|---|---|---|
shared/ui/ | Reusable UI primitives | Badge, Dialog, Select, Tabs (shadcn) |
shared/model/ | Hooks, stores, context | TransportContext, app-store, useTheme |
shared/lib/ | Domain-agnostic utilities | cn(), font-config, celebrations |
entities/session/ | Session domain hooks | useSessionId, useSessions |
entities/runtime/ | Runtime capabilities | useRuntimeCapabilities |
entities/agent/ | Agent identity hooks | useCurrentAgent, useAgentVisual |
entities/tasks/ | Task scheduler hooks | useTasksEnabled, useSchedules, useRuns |
entities/relay/ | Relay messaging hooks | useRelayEnabled, useRelayMessages |
entities/mesh/ | Mesh discovery hooks | useMeshEnabled, useRegisteredAgents |
features/chat/ | Chat interface | ChatPanel, MessageList, ToolCallCard |
features/session-list/ | Session management | SessionSidebar, SessionItem |
features/settings/ | Settings UI | SettingsDialog |
features/tasks/ | Task scheduler UI | TasksPanel, CreateScheduleDialog |
features/relay/ | Relay messaging UI | RelayPanel, AdapterCard |
features/mesh/ | Mesh discovery UI | MeshPanel, TopologyGraph |
features/canvas/ | Agent-controlled UI panels | Canvas renderer, extension host |
entities/marketplace/ | Marketplace data hooks | useMarketplacePackages, useInstallPackage, usePermissionPreview |
features/marketplace/ | Dork Hub browse and install UI | DorkHub, PackageCard, PackageDetailSheet, InstallConfirmationDialog |
widgets/marketplace/ | Marketplace pages | DorkHubPage (/marketplace), MarketplaceSourcesPage (/marketplace/sources) |
widgets/app-layout/ | App-level layout | PermissionBanner |
Layer imports are strictly unidirectional: shared ← entities ← features ← widgets ← app.
Cross-feature model/hook imports are forbidden.
State Management
- Zustand for UI state (sidebar, theme, etc.) —
layers/shared/model/app-store.ts - TanStack Query for server state (sessions, messages, commands, schedules)
- URL Parameters (standalone mode) —
?session=and?dir=persist state in the URL
Data Flow: Message from UI to Claude and Back
User types message
↓
ChatPanel → useChatSession.handleSubmit()
↓
transport.sendMessage(sessionId, content, onEvent, signal, cwd)
↓
fetch(POST /api/sessions/:id/messages) + ReadableStream SSE parsing
↓
Express route → runtimeRegistry.getDefault().sendMessage() → SDK query()
↓
SDK yields StreamEvent objects → SSE wire format
↓
HttpTransport parses SSE → calls onEvent(event)
↓
React state updates → UI re-renders with new message chunksUser types message
↓
ChatPanel → useChatSession.handleSubmit()
↓
transport.sendMessage(sessionId, content, onEvent, signal, cwd)
↓
DirectTransport → runtime.sendMessage() → SDK query()
↓
SDK yields AsyncGenerator<StreamEvent>
↓
DirectTransport iterates generator → calls onEvent(event)
↓
React state updates → UI re-renders with new message chunksStreamEvent Types
Prop
Type
Session Sync Protocol
Subscribe to real-time session changes via GET /api/sessions/:id/stream (persistent SSE). This enables multi-client sync — CLI writes appear in the DorkOS UI automatically.
Events:
sync_connected— Sent on initial connection. Data:{ sessionId }sync_update— Sent when new content is written to the session's JSONL file. Data:{ sessionId, timestamp }
On sync_update, re-fetch message history. GET /messages supports ETag caching for efficient polling.
Testing
Tests use Vitest with vi.mock() for Node modules. Client tests inject mock Transport objects via TransportProvider:
import { createMockTransport } from '@dorkos/test-utils'
const mockTransport = createMockTransport({
listSessions: vi.fn().mockResolvedValue([]),
sendMessage: vi.fn(),
})
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<TransportProvider transport={mockTransport}>
{children}
</TransportProvider>
)
}
render(<MyComponent />, { wrapper: Wrapper })This pattern provides type safety and explicit test setup without global mocks.
Key Design Properties
- Same React app, two deployment modes — Transport abstraction enables full code reuse
- No network required for Obsidian — DirectTransport runs entirely in-process
- Type-safe end-to-end — Zod schemas generate both TypeScript types and OpenAPI specs
- Testable — Mock Transport objects make testing React hooks and components straightforward
- Real-time sync — SSE streaming and file watching keep all clients in sync
- SDK-first storage — JSONL transcripts are the single source of truth (no separate database)