Skip to content

Architecture

Sia edited this page May 30, 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
        │   └── Totp (v0.26.0+)       # RFC 6238 self-impl + Base32 (no external deps)
        ├── audit/                   # AuditLogger + /audit page (v0.15.0+)
        ├── claude/                  # ClaudeSessionManager (stream-json)
        │   ├── ConversationHistoryService (v0.16.0+) — turn persistence
        │   ├── HistoryRoutes        # /projects/{id}/history + /chat/history
        │   ├── GlobalHistorySearchRoutes (v0.30.0+) — /history cross-project grep
        │   ├── ConversationExportService (v0.31.0+) — JSON envelope export/import
        │   ├── ConversationArchiver (v0.33.0+) — 30-day inactive dump-and-prune
        │   ├── PromptSuggestionService (v0.31.0+) — LIKE-prefix autocomplete
        │   └── ClaudeUsageMonitor (v0.21.0+) — quota polling + threshold alert
        ├── env/                     # EnvSetupService, MCP, Claude auth
        │   ├── AgentRegistry (v0.31.0+) — ~/.claude/agents/*.md CRUD
        │   └── AgentRoutes           # /agents + /api/agents (v0.36.0+ JSON dispatch list)
        ├── git/                     # GitReader, GitCloneService, GitWriter (v0.18.0+)
        ├── projects/                # ProjectService, KeystoreGenerator, ProjectTemplates (v0.18.0+)
        │   ├── ProjectArchiver (v0.29.0+) — source zip
        │   ├── EnvFilesRoutes (v0.32.0+) — /projects/{id}/env-files whitelist editor
        │   ├── CodeStatsService (v0.35.0+) — LoC / 언어 분류
        │   ├── CodeSearchService (v0.35.0+) — workspace grep
        │   └── CodeAnalysisRoutes    # /projects/{id}/wrapper + /stats + /code-search
        ├── build/                   # BuildService (Gradle assembleDebug)
        │   ├── BuildCacheService (v0.28.0+) — Gradle/Android/npm cache size + cleanup
        │   ├── buildCacheRoutes      # /settings/cache
        │   ├── DependencyAudit (v0.32.0+) — gradlew :{module}:dependencies parser
        │   ├── DependencyAuditRoutes # /projects/{id}/deps
        │   ├── BuildScheduler (v0.33.0+) — HH:MM / *:MM cron tick (60s)
        │   ├── BuildAutomationRoutes # /projects/{id}/automation + /api/webhooks/build/{id}
        │   └── GradleWrapperService (v0.35.0+) — distributionUrl atomic 교체
        ├── artifacts/               # APK storage
        │   └── ApkSignerInspector (v0.28.0+) — apksigner verify wrapper
        ├── files/                   # Upload routes + ProjectFileBrowser
        ├── prompts/                 # Prompt template store + /prompts page
        ├── notify/                  # EmailNotifier + WebhookNotifier + Notifiers facade
        │   ├── EmailSettingsRoutes  # /settings/email (v0.17.0+)
        │   └── WebhookSettingsRoutes # /settings/webhook (v0.27.0+)
        ├── publish/                 # PlayPublishService + TestFlightPublishService
        │   └── (v0.22.0/v0.23.0)     # MCP delegation, prompts to Claude session
        ├── emulator/                # EmulatorService + /emulator
        │   ├── (v0.19.0 → v0.24.0)   # diagnostics → AVD lifecycle (create/launch/stop)
        │   └── VncProxyRoutes (v0.42.0+) — /emulator/vnc HTTP+WS reverse proxy (admin auth)
        ├── disk/                    # DiskMonitor + dashboard card (v0.29.0+)
        ├── admin/                   # SSR routes + HTML templates
        │   ├── TwoFactorRoutes      # /2fa enable / disable (v0.26.0+)
        │   ├── LogSearchRoutes (v0.32.0+) — /logs grep across all build logs
        │   ├── BackupRoutes (v0.34.0+) — /backup + tar.gz stream
        │   └── MultiConsoleRoutes (v0.36.0+) — /multi-console iframe grid
        │       (UsersRoutes / ProjectAclRoutes removed in v1.45.0 — single-admin)
        ├── tasks/                   # TaskQueue (background work)
        ├── ws/                      # LogHub (WebSocket broadcaster)
        ├── config/                  # ServerConfig + ConfigPersistence
        └── db/                      # VibeDb (PostgreSQL via Exposed)
                                     # schemas (v0.33.0+): build_schedules, build_webhook_secrets

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 / publish / emulator / 2FA / session timeout). See the Audit Log page for the schema and filter URL recipes.
  • admin_users.totp_secret + totp_enabled_at (v0.26.0+) — 2FA TOTP secret + enablement timestamp. See Two-Factor Auth.
  • build_schedules (v0.33.0+) — cron expression + variant + enabled flag + lastFiredAt. See Build Automation.
  • build_webhook_secrets (v0.33.0+) — secret-id + SHA-256 hash + lastUsedAt for external trigger auth.

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.
  • 2FA TOTP (v0.26.0+): when enabled, login requires a 6-digit code after password. Server returns 401 totp_required on first call, expects totpCode field on the retry.
  • Session idle timeout (v0.26.0+): security.sessionIdleTimeoutMinutes (default 30) auto-deletes device rows whose lastSeenAt exceeded the threshold. Enforced both in AuthPlugin (Bearer) and SSR requireSessionOrRedirect.
  • Single-admin (multi-user removed in v1.45.0): roles (admin/member/viewer), /users management and Project ACL were removed — single-operator tool. WebSession.isAdmin / canWrite and DevicePrincipal.isAdmin / canWrite are always true; the requireAdminOrRedirect / requireWriteAccessOrRedirect / requireApiWrite / requireApiAdmin / requireProjectAcl helpers remain as inert pass-throughs. Authentication is the only access boundary.

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.
  • v0.16.0 — GET /projects/{id}/history + GET /chat/history (conversation_turns).
  • v0.18.0 — ApiPath.gitCommit(projectId) + templateId on project register.
  • v0.20.0 — ApiPath.PROMPT_TEMPLATES + PromptTemplateDto / PromptTemplateListResponseDto.
  • v0.21.0 — ClaudeStatusDto.usagePercent + resetAt (additive).
  • v0.26.0 — LoginRequestDto.totpCode (additive). 2FA-enabled accounts signal 401 totp_required until the field is supplied.
  • v0.31.0 — GET /api/projects/{id}/claude/prompt-suggestions?prefix=... added (server-only — JSON shape is the simple {"suggestions": [...]} map; no DTO promoted to shared/ yet).
  • v0.33.0 — POST /api/webhooks/build/{projectId} external trigger endpoint (admin-auth-free, multi-secret via X-Vibe-Secret-Id + X-Vibe-Secret + optional X-Vibe-Signature).
  • v0.36.0 — GET /api/agents Bearer JSON (list ~/.claude/agents/*.md for console UI / Android dispatch dropdown).
  • v0.44.0 — SubAgentSessionManager + per-agent SSR consoles + REST (POST /api/projects/{id}/agents/{agent}/console/prompt | cancel, GET /api/projects/{id}/agents/active) + WS (/ws/projects/{id}/agents/{agent}/console/logs). Independent Claude child processes per (projectId, agentName).
  • v0.45.0 — JSON API + WebSocket role guards (requireApiWrite / requireApiAdmin extensions on ApplicationCall; WS viewer_readonly short-circuit for UserPrompt / ActionInvoke). (removed in v1.45.0 — single-admin; guards now inert pass-throughs)
  • v0.46.0 — WebPushNotifier (VAPID P-256 ECDSA + RFC 8292 JWT) + PushSubscriptionRepository + /settings/push SSR + /api/push/{vapid-public-key, subscribe, subscriptions/{id}}. Notifiers facade extended with webPush channel.
  • v0.47.0 — admin guard sweep across /settings/* SSR pages. /usage admin-only viewer for the cached Claude /status raw output (ClaudeStatusService.rawSnapshots). Helm chart at helm/vibe-coder-server/.
  • v0.48.0 — WebauthnService (wraps webauthn4j 0.29.1) + WebauthnCredentialRepository + WebauthnSection config + /webauthn SSR + 4 JSON endpoints (POST /api/webauthn/{register,assert}/{options,verify}). Login page integrates passkey-only flow next to password / TOTP.
  • v0.49.0 — Project ACL (ProjectAcls table, ProjectAclRepository, requireProjectAccessOrRedirect, /users/{userId}/projects editor) — removed in v1.45.0. The conversation_turns.agent_name column (still present) lets SubAgentSessionManager persist its turns alongside the main console (ConversationHistoryService API gains agentName: String?).
  • v0.50.0 — Aes128GcmEncrypt (pure JDK stdlib) implements RFC 8291 aes128gcm content-encoding for WebPushNotifier. Service worker reads decrypted event.data.json() payload (title / body / url) and routes click to the matching open tab.
  • v0.51.0 — ApplicationCall.requireProjectAcl(projects, projectId) JSON guard + console / sub-agent WebSocket ACL handshakes. (removed in v1.45.0 — single-admin; endpoints now require only auth) buildRoutes / fileRoutes / wsRoutes signatures gain ProjectService parameter.
  • v0.52.0 — ConversationTurnRepository.Filter.agentName (3-mode: null main only / "" all / "<name>" specific) + distinctAgents(projectId) helper. /history SSR + chat-history gain dropdown + row badge; pagination round-trips agent= param.
  • v0.53.0 — content_tsv PG-12 GENERATED ALWAYS AS STORED column + GIN index on conversation_turns. Database.init() emits idempotent ALTER TABLE … ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS raw SQL. TsvectorMatchOp (private Op<Boolean> with registerArgument) replaces LIKE in Filter.q.
  • v0.54.0 — SymbolFinder (Kotlin/Java regex-based definition lookup) + symbolRoutes (GET /projects/{id}/symbols SSR + GET /api/projects/{id}/symbols?name=). File viewer JS reads ?line=N query and smooth-scrolls + outlines the target line.
  • v0.55.0 — MetricsRegistry + /metrics admin SSR (Prometheus text exposition; zero deps).
  • v0.56.0 — RateLimiter + installRateLimit Ktor plugin (/api/, /ws/, /login only; admin bypass; 429 + Retry-After). Config security.rateLimit.*.
  • v0.57.0 — admin_users.passwordless_only flag + /webauthn toggle. AuthService.login(hasPasskey) callback. Helm fullImage.enabled opt-in (:full tag + /dev/kvm + privileged
    • noVNC port).
  • v0.58.0 — BuildService.compareWithPrevious(...) + SSR comparison card on build detail page.
  • v0.59.0 — BuildService.statistics(...) + SSR builds-list stats card (success rate / avg duration / inline SVG sparkline + APK size trend).
  • v0.60.0 — BackupService (extracted from BackupRoutes) + BackupScheduler cron polling. New SSR endpoints /backup/auto/{name}, /backup/auto/{name}/delete, /backup/auto/run-now.
  • v0.61.0 — conversation_turns.user_memo (text) + starred (bool). Repository setMemo / setStarred / findById. Filter starredOnly. SSR row gains ☆/★ + memo editor; JSON endpoints POST .../history/{turnId}/star|memo (CSRF via ?_csrf=).
  • v0.62.0 — CREATE EXTENSION IF NOT EXISTS pg_trgm + GIN gin_trgm_ops on content. TrigramIlikeOp (private Op<Boolean>) — auto-routes non-ASCII queries.
  • v0.63.0 — ClaudeEvent.UsageReport + ClaudeStreamParser reads message.usage (assistant frames) and top-level usage (result frames). Persisted as role="usage" history rows. ConversationTurnRepository.usageSummary(projectId) aggregates. Console + sub-agent toWsFrame exposes the report as a small ConsoleSystem(code="usage") notice. /usage page gains a structured cache stats card.

Background services (started in ServerMain)

Service Started Polls Shutdown hook
ClaudeUsageMonitor v0.21.0 claude /status every 5 min yes
DiskMonitor v0.29.0 Files.getFileStore(root) every 10 min yes
BuildScheduler v0.33.0 enabled build_schedules every 60 s yes
ConversationArchiver v0.33.0 conversation_turns once every 24 h yes
Notifiers (email + webhook + webPush) v0.27.0 / v0.46.0 n/a (event-driven) yes
ClaudeSessionManager from v0.4.0 per-project child processes yes
SubAgentSessionManager v0.44.0 per (project, agent) child processes; persists turns since v0.49.0 yes
WebPushNotifier v0.46.0 / v0.50.0 n/a (event-driven; lazy VAPID keypair; aes128gcm encrypted since v0.50.0) n/a (JDK HttpClient close on JVM exit)
WebauthnService v0.48.0 n/a (per-request; 5 min in-memory challenge TTL) n/a
MetricsRegistry v0.55.0 n/a (sampled on each /metrics scrape) n/a
RateLimiter v0.56.0 n/a (in-memory per-IP token buckets) n/a (state lost on restart by design)
BackupScheduler v0.60.0 enabled backup.cron every 60 s yes

All are wired in ServerMain.kt and added to a single Runtime.getRuntime().addShutdownHook(...) so docker compose stop cleans up gracefully.

Clone this wiki locally