Skip to content

Architecture

wody edited this page May 23, 2026 · 19 revisions

Architecture Overview

Process model

┌──────────────────────────────────────────────────────────────────┐
│  docker compose stack                                            │
│                                                                   │
│  ┌────────────────────────────────────┐   ┌──────────────────┐  │
│  │  vibe-coder-server                 │   │ vibe-coder-      │  │
│  │  (Ubuntu 24.04 LTS, port 17880)    │   │  postgres        │  │
│  │  • Ktor / Netty                     │◄─►│  (postgres:17-  │  │
│  │  • Routes (admin SSR + JSON API)   │   │   alpine)        │  │
│  │  • Exposed ORM → PostgreSQL         │   │  • Port 5432    │  │
│  │  • WebSocket log hub                │   │   (internal)    │  │
│  └─────────────┬──────────────────────┘   └──────────────────┘  │
│                │ spawns                                            │
│        ┌───────┼────────┬────────┐                                │
│        ▼       ▼        ▼        ▼                                │
│     claude  gradlew    git    vibe-doctor                         │
│     (per-   (per-     (read-  (Android SDK,                       │
│      project build)   only)   MCP install)                        │
│      persistent                                                    │
│      child)                                                        │
└──────────────────────────────────────────────────────────────────┘

All external commands are wrapped in a TaskQueue + LogHub so progress streams uniformly to WebSocket clients (browser / Android). The PostgreSQL sidecar (v0.14.0+) holds admin / projects / builds / artifacts / uploaded_files. The server connects via JDBC over the internal docker network — no port is exposed to the host by default.

Module layout

vibe-coder-server/
├── shared/                          # JVM library (DTOs, ApiPath, WsFrame)
│   └── src/main/kotlin/.../shared/
│       ├── ApiPath.kt               # All REST/WS routes as constants
│       ├── ws/WsFrame.kt            # Sealed class hierarchy for WS frames
│       └── dto/Dtos.kt              # @Serializable request/response types
│
└── server/                          # Ktor app body
    └── src/main/kotlin/.../server/
        ├── ServerMain.kt            # Bootstrap, DI wiring
        ├── Module.kt                # Routing + plugin install
        ├── auth/                    # Bearer + session + setup + CSRF
        ├── audit/                   # AuditLogger + /audit page (v0.15.0+)
        ├── claude/                  # ClaudeSessionManager (stream-json)
        ├── env/                     # EnvSetupService, MCP, Claude auth
        ├── git/                     # GitReader, GitCloneService, credentials
        ├── projects/                # ProjectService, KeystoreGenerator
        ├── build/                   # BuildService (Gradle assembleDebug)
        ├── artifacts/               # APK storage
        ├── files/                   # Upload routes + ProjectFileBrowser
        ├── prompts/                 # Prompt template store + /prompts page
        ├── admin/                   # SSR routes + HTML templates
        ├── tasks/                   # TaskQueue (background work)
        ├── ws/                      # LogHub (WebSocket broadcaster)
        ├── config/                  # ServerConfig + ConfigPersistence
        └── db/                      # VibeDb (PostgreSQL via Exposed)

Data flow — example: send a prompt

  1. Client sends POST /api/projects/{id}/claude/console/prompt with text.
  2. ConsoleRoutes finds or spawns the claude child for that project (ClaudeSessionManager.spawnSession). Stream-json mode (--output-format stream-json --input-format stream-json).
  3. The user prompt is written as a stream-json frame to the child's stdin.
  4. Claude responds line-by-line on stdout. ClaudeStreamParser decodes each line and turns it into a WsFrame subtype:
    • console_session_started
    • console_assistant (with isPartial)
    • console_tool_use / console_tool_result
    • console_done / console_error
  5. LogHub broadcasts the frame to all WS subscribers on /ws/projects/{id}/console/logs.
  6. Browser console UI renders incrementally. Android client does the same with the same JSON shape.

To cancel a turn (v0.13.0+): POST .../claude/console/cancel — server sends SIGTERM to the child but keeps the saved session-id, so the next prompt resumes the same conversation.

Persistence

All persistent state lives under one host directory (v0.7.0+) and the PostgreSQL data directory is part of that tree (v0.14.0+). See Data Volumes & Backup for the full mapping.

./vibe-coder-data/
├── workspace/         # project sources + APKs
├── postgres/          # PostgreSQL data dir (v0.14.0+)
├── server/            # server logs + build metadata
├── dev-tools/         # Android SDK, Gradle, npm-global (MCP), npm cache, ...
└── claude/            # Claude OAuth credentials + MCP registrations

The image itself contains only the server body (~600 MB) and is replaced on upgrade. Every persistent path is a bind mount; no Docker named volumes are used by default. The PostgreSQL directory is owned by UID 70 inside the container (the postgres user in alpine images) — see Data-Volumes for backup procedures.

Database layer (v0.14.0+)

  • Engine: PostgreSQL 17 (postgres:17-alpine sidecar container).
  • ORM: Exposed 0.55.0 + Hikari connection pool (default size 10).
  • Tables: admin_users, devices, projects, builds, artifacts, uploaded_files. Schema is created/migrated on boot via SchemaUtils.createMissingTablesAndColumns.
  • Cascade: Foreign keys reference projects.id. PostgreSQL enforces these (unlike SQLite's default off). ProjectService.delete does explicit cascade cleanup for uploaded_files, artifacts, builds before deleting the project row.
  • Connection retry: On boot, the server retries 30× / 2 s = 60 s total to give the postgres container time to become healthy.
  • Future: conversation_turns table (Roadmap §F.☆ #1) — JSONB column for tool_use input/output, GIN tsvector for full-text search.
  • Audit log (v0.15.0+): audit_log table records IAM-level actions (auth / project / build / MCP / settings / git / console). See the Audit Log page for the schema and filter URL recipes.

Pre-v0.14.0 used SQLite single-writer; see the Upgrade Guide v0.13 → v0.14 section for the migration story.

Auth boundary

  • First boot: empty DB → /setup form creates admin (or VIBECODER_ADMIN_USERNAME/PASSWORD env auto-bootstrap).
  • Login: /api/auth/login returns bearer token + vibe_session cookie.
  • Subsequent requests: either auth header (Authorization: Bearer ...) or the cookie. Both paths converge in the same installAuth plugin.
  • CSRF: All SSR POST forms carry an HMAC-SHA256-derived CSRF token in a hidden _csrf input (v0.12.4+). REST API (Bearer header) is exempt.
  • Passwords: BCrypt cost 12 hash. 10 failures → 15-min account lock, 30 failures from same IP / 24 h → 24-h IP block (v0.12.4+). Timing-safe dummy verify on missing users.

Wire stability

shared/ is the contract between server and Android client. All wire changes (ApiPath / DTO / WsFrame) must be reflected in the Android companion repo's shared/ copy. CHANGELOG marks them with Wire change: Yes/No. Recent additions:

  • v0.10.0 — RegisterProjectRequestDto clone fields + 19 env-setup APIs.
  • v0.13.0 — ApiPath.claudeConsoleCancel(projectId) for turn cancel.

Clone this wiki locally