-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ 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.
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
-
Client sends
POST /api/projects/{id}/claude/console/promptwith text. -
ConsoleRoutes finds or spawns the
claudechild for that project (ClaudeSessionManager.spawnSession). Stream-json mode (--output-format stream-json --input-format stream-json). - The user prompt is written as a stream-json frame to the child's stdin.
- Claude responds line-by-line on stdout.
ClaudeStreamParserdecodes each line and turns it into aWsFramesubtype:console_session_started-
console_assistant(withisPartial) -
console_tool_use/console_tool_result -
console_done/console_error
-
LogHubbroadcasts the frame to all WS subscribers on/ws/projects/{id}/console/logs. - 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.
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.
-
Engine: PostgreSQL 17 (
postgres:17-alpinesidecar 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 viaSchemaUtils.createMissingTablesAndColumns. -
Cascade: Foreign keys reference
projects.id. PostgreSQL enforces these (unlike SQLite's default off).ProjectService.deletedoes explicit cascade cleanup foruploaded_files,artifacts,buildsbefore 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_turnstable (Roadmap §F.☆ #1) — JSONB column fortool_useinput/output, GIN tsvector for full-text search. -
Audit log (v0.15.0+):
audit_logtable 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.
-
First boot: empty DB →
/setupform creates admin (orVIBECODER_ADMIN_USERNAME/PASSWORDenv auto-bootstrap). -
Login:
/api/auth/loginreturns bearer token +vibe_sessioncookie. -
Subsequent requests: either auth header (
Authorization: Bearer ...) or the cookie. Both paths converge in the sameinstallAuthplugin. -
CSRF: All SSR POST forms carry an HMAC-SHA256-derived CSRF token in
a hidden
_csrfinput (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_requiredon first call, expectstotpCodefield on the retry. -
Session idle timeout (v0.26.0+):
security.sessionIdleTimeoutMinutes(default 30) auto-deletes device rows whoselastSeenAtexceeded the threshold. Enforced both inAuthPlugin(Bearer) and SSRrequireSessionOrRedirect. -
Single-admin (multi-user removed in v1.45.0): roles
(
admin/member/viewer),/usersmanagement and Project ACL were removed — single-operator tool.WebSession.isAdmin/canWriteandDevicePrincipal.isAdmin/canWriteare alwaystrue; therequireAdminOrRedirect/requireWriteAccessOrRedirect/requireApiWrite/requireApiAdmin/requireProjectAclhelpers remain as inert pass-throughs. Authentication is the only access boundary.
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 —
RegisterProjectRequestDtoclone 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)+templateIdon 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 signal401 totp_requireduntil 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 toshared/yet). - v0.33.0 —
POST /api/webhooks/build/{projectId}external trigger endpoint (admin-auth-free, multi-secret viaX-Vibe-Secret-Id+X-Vibe-Secret+ optionalX-Vibe-Signature). - v0.36.0 —
GET /api/agentsBearer JSON (list~/.claude/agents/*.mdfor 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/requireApiAdminextensions onApplicationCall; WSviewer_readonlyshort-circuit forUserPrompt/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/pushSSR +/api/push/{vapid-public-key, subscribe, subscriptions/{id}}.Notifiersfacade extended withwebPushchannel. - v0.47.0 — admin guard sweep across
/settings/*SSR pages./usageadmin-only viewer for the cached Claude/statusraw output (ClaudeStatusService.rawSnapshots). Helm chart athelm/vibe-coder-server/. - v0.48.0 —
WebauthnService(wrapswebauthn4j0.29.1) +WebauthnCredentialRepository+WebauthnSectionconfig +/webauthnSSR + 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 (
ProjectAclstable,ProjectAclRepository,requireProjectAccessOrRedirect,/users/{userId}/projectseditor) — removed in v1.45.0. Theconversation_turns.agent_namecolumn (still present) letsSubAgentSessionManagerpersist its turns alongside the main console (ConversationHistoryServiceAPI gainsagentName: String?). - v0.50.0 —
Aes128GcmEncrypt(pure JDK stdlib) implements RFC 8291aes128gcmcontent-encoding forWebPushNotifier. Service worker reads decryptedevent.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/wsRoutessignatures gainProjectServiceparameter. - v0.52.0 —
ConversationTurnRepository.Filter.agentName(3-mode:nullmain only /""all /"<name>"specific) +distinctAgents(projectId)helper./historySSR + chat-history gain dropdown + row badge; pagination round-tripsagent=param. - v0.53.0 —
content_tsvPG-12GENERATED ALWAYS AS STOREDcolumn + GIN index onconversation_turns.Database.init()emits idempotentALTER TABLE … ADD COLUMN IF NOT EXISTS+CREATE INDEX IF NOT EXISTSraw SQL.TsvectorMatchOp(privateOp<Boolean>withregisterArgument) replaces LIKE inFilter.q. - v0.54.0 —
SymbolFinder(Kotlin/Java regex-based definition lookup) +symbolRoutes(GET /projects/{id}/symbolsSSR +GET /api/projects/{id}/symbols?name=). File viewer JS reads?line=Nquery and smooth-scrolls + outlines the target line. - v0.55.0 —
MetricsRegistry+/metricsadmin SSR (Prometheus text exposition; zero deps). - v0.56.0 —
RateLimiter+installRateLimitKtor plugin (/api/,/ws/,/loginonly; admin bypass; 429 +Retry-After). Configsecurity.rateLimit.*. - v0.57.0 —
admin_users.passwordless_onlyflag +/webauthntoggle.AuthService.login(hasPasskey)callback. HelmfullImage.enabledopt-in (:fulltag +/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 fromBackupRoutes) +BackupSchedulercron 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). RepositorysetMemo/setStarred/findById. FilterstarredOnly. SSR row gains ☆/★ + memo editor; JSON endpointsPOST .../history/{turnId}/star|memo(CSRF via?_csrf=). - v0.62.0 —
CREATE EXTENSION IF NOT EXISTS pg_trgm+ GINgin_trgm_opsoncontent.TrigramIlikeOp(privateOp<Boolean>) — auto-routes non-ASCII queries. - v0.63.0 —
ClaudeEvent.UsageReport+ClaudeStreamParserreadsmessage.usage(assistant frames) and top-levelusage(result frames). Persisted asrole="usage"history rows.ConversationTurnRepository.usageSummary(projectId)aggregates. Console + sub-agenttoWsFrameexposes the report as a smallConsoleSystem(code="usage")notice./usagepage gains a structured cache stats card.
| 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.