server-architecture
Skill9 starsChanged 17 days agoApache-2.0View on GitHub
ShipIt server/orchestrator architecture: buildApp(), HTTP API routes, services layer, WebSocket handler dispatch, SSE broadcasts, dependency injection, state scopes, manager initialization, type system. Load when working on orchestrator code, routes, services, or WebSocket handlers.
- Reads credentials
In this repository15 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- add-endpointSkill
- android-buildSkill
- client-architectureSkill
- deployment-architectureSkill
- design-languageSkill
- docs-navigatorSkill
- dogfooding-shipitSkill
- frontend-design:frontend-designSkill
- git-architectureSkill
- prompt-architectureSkill
- session-containersSkill
- session-lifecycleSkill
- session-processesSkill
- testing-and-qualitySkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 133
Call activateSession(sessionId) — gets or creates a runner, attaches the connection
- Very specific: line 173
Resolve runners via the registry: use resolveRunner(ctx) from ws-handlers/resolve-runner.ts (or …
- Very specific: line 196
All types live in src/server/shared/types/:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Server Architecture
The server is a single Fastify process (the orchestrator) that handles HTTP, WebSocket, and SSE connections from the browser. It delegates session-scoped work to Docker containers running session workers.
Entry Point: buildApp()
src/server/orchestrator/index.ts exports buildApp(deps: AppDeps), which:
- Instantiates all managers (or accepts injected stubs from
deps) - Initializes global git config and agent detection
- Sets up Docker container manager (production) or skips it (tests)
- Creates the session runner registry with a factory
- Registers HTTP routes via
registerApiRoutes() - Registers WebSocket handler at
/ws/sessions/:id - Registers SSE endpoint at
/api/events - Registers preview proxy routes
- Sets up startup tasks (warm pool validation, orphan cleanup)
- Returns the Fastify instance without starting it
The function returns the app without calling listen(), so integration tests can use app.inject() without binding a port.
Dependency Injection
buildApp() accepts an AppDeps object where every field is optional. Production uses real implementations; tests supply mocks/stubs. This is the foundation of testability — integration tests never spawn real Docker containers, Claude CLI processes, or Vite servers.
Key injectable dependencies:
| Dependency | Type | Purpose |
createGitManager | (dir) => GitManager | Per-session git operations |
createRepoGit | (dir) => RepoGit | Bare cache + per-session local-clone ops |
sessionManager | SessionManager | Session metadata persistence |
authManager | AuthManager | Claude CLI OAuth |
githubAuthManager | GitHubAuthManager | GitHub token + API |
credentialStore | CredentialStore | Unified credentials |
agentFactory | (agentId) => AgentProcess | Agent process creation (test-only) |
runnerFactory | SessionRunnerFactory | Session runner creation |
sessionContainerManager | SessionContainerManager | Docker orchestration |
In production, agents live inside session containers — the orchestrator never spawns them directly. The agentFactory dependency exists only for integration tests.
Manager Initialization Order
createGitManager / createRepoGit (factories)
SessionManager (session metadata)
RepoStore (imported repos)
ChatHistoryManager (per-session messages)
UsageManager (cost tracking)
AuthManager (Claude CLI OAuth)
CredentialStore (unified credentials)
initGlobalGitConfig() (GIT_CONFIG_GLOBAL)
AgentRegistry (detect installed CLIs)
GitHubAuthManager (GitHub token + API)
markdown.ts (scan workspace for docs/ markdown)
SessionContainerManager (Docker, production only)
SessionRunnerRegistry (active runners)State Scopes
State is managed at three scopes:
App-Level (server lifetime)
Singleton managers created in buildApp(). Shared across all connections.
SessionManager— session metadata (title, workspace dir, remote URL, warm flag)RepoStore— imported repos, clone status, warm session IDsSessionRunnerRegistry— active runners (no count cap; reclaim is memory-budget driven, docs/284)SessionContainerManager— Docker containersCredentialStore— git identity, GitHub token, agent auth (one encrypted file)AuthManager/GitHubAuthManager— authentication stateAgentRegistry— detected agent CLIs
Per-Connection (WebSocket lifetime)
State bound to a single browser tab's WebSocket connection, tracked in closure variables inside the WS handler.
- Active session ID and directory
- Active agent ID
- Attached runner reference
- Log buffer
Per-Session (runner lifetime)
State on the SessionRunnerInterface, shared across all connections viewing the same session.
- Agent process and running state
- Message queue (queued prompts waiting for current turn to finish)
- Turn event buffer (replayed to new viewers)
- Terminal process
- Preview status
- Viewer count
HTTP API (api-routes*.ts)
Routes are registered via registerApiRoutes() (api-routes.ts, called from route-registry.ts) and split by
domain across api-routes-*.ts. All are prefixed /api. For the live set, grep the route files
(grep -n "app\.\(get\|post\|patch\|put\|delete\)" src/server/orchestrator/api-routes*.ts) rather than trusting a copied table.
Error Handling
Service functions throw ServiceError(statusCode, message). A Fastify onError hook catches these and returns the appropriate HTTP status with a JSON body:
{ "error": "Session not found" }Services Layer (services/)
Business logic lives in src/server/orchestrator/services/ as pure exported functions. Each function accepts explicit parameters (manager references, IDs) and returns data or throws ServiceError.
ls src/server/orchestrator/services/ for the current set; types.ts holds ServiceError and the
bootstrap/settings shapes.
Services are consumed by both HTTP routes and WebSocket handlers. This keeps business logic testable independently of transport.
WebSocket Handler Architecture
Connection Lifecycle
The client connects to ws[s]://host/ws/sessions/{sessionId}?agent=claude. On connection:
- Validate session exists (close with 4004 if not)
- Initialize per-connection state (closures in the WS handler)
- Call
activateSession(sessionId)— gets or creates a runner, attaches the connection - Send log buffer and current status
Message Dispatch
The switch (msg.type) dispatcher lives in route-registry.ts (not index.ts) and narrows
WsClientMessage (shared/types/ws-client-messages.ts) to per-file handlers in ws-handlers/.
Handler Context
Handlers receive a ctx object combining three interfaces (see ws-handlers/types.ts):
ConnectionCtx — per-connection communication and session management:
send(),broadcastLog(),sseBroadcast()getActiveDir(),getActiveSessionDir(),getActiveAppSessionId()activateSession(),checkGitIdentity(),scheduleAutoPush()
RunnerCtx — per-session runner delegation:
agentFactory(agentId)getActiveAgentId()/setActiveAgentId(),getSelectedModel()/setSelectedModel(),getSelectedReasoning()/setSelectedReasoning()— per-connection identifiers that don't depend on runner stategetRunner(),getRunnerRegistry(),attachToRunner(),detachFromRunner()
Turn state is NOT on the context. There are no getIsClaudeRunning(), getAccumulatedText(), getTurnSummary(), getChatMessageGroups(), getMessageQueue(), or clearMessageQueue() accessors — they were removed because they silently no-op'd after a WS disconnect. Resolve a runner with resolveRunner(ctx) (which prefers the registry) and read/write it directly: runner.running, runner.turnSummary = "…". See CLAUDE.md → WebSocket lifecycle MUST NOT affect server behavior.
AppCtx — app-level managers and factories:
sessionManager,chatHistoryManager, etc.createGitManager(),createRepoGit(),createSessionDir()workspaceDir,sessionsRoot,defaultAgentId
Handler Files
One file per domain in ws-handlers/ (send-message.ts, terminal-handlers.ts, misc-handlers.ts, …); ls it and read the route-registry.ts switch for the message→handler map.
Critical rule: WebSocket lifecycle MUST NOT affect server behavior
WS disconnects and reconnects are routine. They MUST NOT stop agents, dispose runners, destroy containers, or corrupt persisted state. Concretely:
socket.on("close")only callsdetachFromRunner(). It must NOT callenforceIdleContainerLimit(),runner.dispose(),agent.kill(),containerManager.destroy(), or anything that affects state. Idle cleanup runs on a periodic timer and acts only when ShipIt is over its memory budget (seeidle-enforcer.ts: createIdleEnforcer, docs/284).runner.dispose()refuses to kill running agents unless{ force: true }is passed. The only callers passingforceare explicit user actions (archive, repo delete, full reset) and shutdown.- Inside async closures (
agent.on("event"|"done"|"error"),setTimeout,Promise.then, recursive turns), capturerunner/capturedSessionId/capturedSessionDirONCE at function entry. Never callctx.getRunner()inside those closures — it returns the per-connectionattachedRunner, which becomes null after disconnect. Mutaterunner.Xdirectly and emit viarunner.emitMessage()(which broadcasts to all viewers AND buffers for reconnects), notctx.send(). - Resolve runners via the registry: use
resolveRunner(ctx)fromws-handlers/resolve-runner.ts(orctx.getRunnerRegistry().get(capturedSessionId)). The registry survives WS disconnects;attachedRunnerdoesn't. RunnerCtxno longer exposessetIsClaudeRunning,setTurnSummary, etc. They were a hazard — silent no-ops after disconnect. The only way to mutate runner state isrunner.X = …on a resolved runner. Seedocs/095-runner-ctx-simplification/plan.md.
SSE Broadcast (/api/events)
The orchestrator maintains a Server-Sent Events endpoint for global push to all connected clients. Events include:
session_list— session created/renamed/archived/graduatedsession_started— new session with initial messagerepo_list— repo added/removed/status changedrepo_status— clone progress, warm readyrepo_warm_ready— warm session available for a repoauth_required/auth_complete— Claude auth flowagent_list— agent availability changedactive_runners— which sessions have running agentsfull_reset_complete— workspace was reset
SSE is separate from per-session WebSocket. It broadcasts to all tabs, not just the active session.
So a field only the open session's viewer reads does not belong on a session list row: list() / listAll() return SessionListRow (no status card, goal, conversation replay or agent notice), and single-session events go through toListRow. The card and the goal reach the open session on its socket as session_details, sent at activation and on each SessionManager.onDetailsChanged (docs/303-session-status-card → Delivery).
Type System
All types live in src/server/shared/types/:
| File | Contents |
index.ts | Barrel re-export |
ws-client-messages.ts | WsClientMessage union |
ws-server-messages.ts | WsServerMessage union (50+ types) |
domain-types.ts | SessionInfo, RepoInfo, ProjectTemplate |
claude-types.ts | ClaudeEvent, NDJSON message types |
agent-types.ts | AgentProcess, AgentEvent, AgentCapabilities |
deployment-types.ts | GitHubDeploymentStatus |
github-types.ts | WsGitHubStatus, WsGitHubPushResult, … (GitHub WS server messages) |
terminal-types.ts | TerminalProcess, WsTerminalStart, … |
usage-types.ts | UsageTurn, TurnUsage, SessionUsage, WeeklyUsage |
attachment-types.ts | ImageAttachment, FileContextRef |
Types are shared between server and client (client imports from ../../server/shared/types.js).
Persistence
Persistence is SQLite (src/server/shared/database.ts), not JSON files. The managers below are thin typed accessors over their tables; adding a persisted field means a table column plus a database.ts migration.
| Data | Storage | Owner |
| Session metadata | sessions table | SessionManager (sessions.ts) |
| Chat history | messages table | ChatHistoryManager (chat-history.ts) |
| Usage stats | usage_turns table | UsageManager (usage.ts) |
| Repos | repos table | RepoStore (repo-store.ts) |
| Secrets | secrets table | SecretStore (secret-store.ts) |
| Reviews, rewind snapshots, egress rules, presentations | their own tables | see database.ts |
| GitHub token, agent env, provider-account metadata | {credentialsDir}/shipit-credentials.json (encrypted when a cipher is supplied, not unconditionally) | CredentialStore (credential-store.ts) |
| Git identity | .gitconfig in the credentials dir, via GIT_CONFIG_GLOBAL — not a CredentialStore field | git-config.ts |
| Provider subscription auth | Per-account filesystem roots (.claude/…, .codex/auth.json) | provider-account-manager.ts |
| Session code | Git repo | /workspace/sessions/{uuid}/ |
Those are four distinct stores — SQLite domain records, the CredentialStore JSON, global git config, and the provider CLI auth roots. Don't collapse them.
Deploy status is not persisted locally — it is read from the GitHub Deployments API (see the deployment-architecture skill). The .vibe-sessions.json / .shipit-usage.json names survive only as file-watcher ignore entries (fs-constants.ts), not as stores. There is no ThreadManager.
Key Files
| File | Role |
src/server/orchestrator/index.ts | buildApp() — app factory, DI setup |
src/server/orchestrator/route-registry.ts | Route registration, WS connection + dispatch switch |
src/server/orchestrator/api-routes.ts | HTTP REST API routes |
src/server/orchestrator/services/*.ts | Business logic (pure functions) |
src/server/orchestrator/ws-handlers/*.ts | WebSocket message handlers |
src/server/orchestrator/ws-handlers/types.ts | ConnectionCtx, RunnerCtx, AppCtx interfaces |
src/server/orchestrator/session-runner.ts | SessionRunnerInterface, SessionRunnerRegistry |
src/server/orchestrator/container-session-runner.ts | ContainerSessionRunner (production runner) |
src/server/orchestrator/session-container.ts | Docker container management |
src/server/session/session-worker.ts | In-container Fastify server |
src/server/session/agents/claude/process.ts | ClaudeProcess — spawns CLI, parses NDJSON (per-agent dirs since docs/155) |
src/server/orchestrator/service-manager.ts | ServiceManager — Docker Compose lifecycle |
src/server/orchestrator/compose-generator.ts | Compose override generation, volume rewriting |
src/server/shared/git.ts | GitManager — per-session git |
src/server/orchestrator/repo-git.ts | RepoGit — bare cache + per-session local-clone ops |
src/client/App.tsx | Main React component |
src/client/stores/*.ts | Zustand stores |
src/client/hooks/*.ts | Custom hooks (WebSocket, API, message handling) |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

