-
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 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 # RFC 6238 self-impl + Base32 (no external deps)
├── audit/ # AuditLogger + /audit page
├── claude/ # ClaudeSessionManager (stream-json)
│ ├── ConversationHistoryService — turn persistence
│ ├── HistoryRoutes # /projects/{id}/history + /chat/history
│ ├── GlobalHistorySearchRoutes — /history cross-project grep
│ ├── ConversationExportService — JSON envelope export/import
│ ├── ConversationArchiver — 30-day inactive dump-and-prune
│ ├── PromptSuggestionService — LIKE-prefix autocomplete
│ └── ClaudeUsageMonitor — quota polling + threshold alert
├── env/ # EnvSetupService, MCP, Claude auth
│ ├── AgentRegistry — ~/.claude/agents/*.md CRUD
│ └── AgentRoutes # /agents + /api/agents JSON dispatch list
├── git/ # GitReader, GitCloneService, GitWriter
├── projects/ # ProjectService, KeystoreGenerator, ProjectTemplates
│ ├── ProjectArchiver — source zip
│ ├── EnvFilesRoutes — /projects/{id}/env-files whitelist editor
│ ├── CodeStatsService — LoC / 언어 분류
│ ├── CodeSearchService — workspace grep
│ └── CodeAnalysisRoutes # /projects/{id}/wrapper + /stats + /code-search
├── build/ # BuildService (Gradle assembleDebug)
│ ├── BuildCacheService — Gradle/Android/npm cache size + cleanup
│ ├── buildCacheRoutes # /settings/cache
│ ├── DependencyAudit — gradlew :{module}:dependencies parser
│ ├── DependencyAuditRoutes # /projects/{id}/deps
│ ├── BuildScheduler — HH:MM / *:MM cron tick (60s)
│ ├── BuildAutomationRoutes # /projects/{id}/automation + /api/webhooks/build/{id}
│ └── GradleWrapperService — distributionUrl atomic 교체
├── artifacts/ # APK storage
│ └── ApkSignerInspector — apksigner verify wrapper
├── files/ # Upload routes + ProjectFileBrowser
├── prompts/ # Prompt template store + /prompts page
├── notify/ # EmailNotifier + WebhookNotifier + Notifiers facade
│ ├── EmailSettingsRoutes # /settings/email
│ └── WebhookSettingsRoutes # /settings/webhook
├── publish/ # PlayPublishService + TestFlightPublishService
│ # MCP delegation, prompts to Claude session
├── device/ # AdbService + /adb wireless ADB logcat
├── disk/ # DiskMonitor + dashboard card
├── admin/ # SSR routes + HTML templates
│ ├── TwoFactorRoutes # /2fa enable / disable
│ ├── LogSearchRoutes — /logs grep across all build logs
│ ├── BackupRoutes — /backup + tar.gz stream
│ └── MultiConsoleRoutes — /multi-console iframe grid
├── tasks/ # TaskQueue (background work)
├── ws/ # LogHub (WebSocket broadcaster)
├── config/ # ServerConfig + ConfigPersistence
└── db/ # VibeDb (PostgreSQL via Exposed)
# schemas: 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: 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, and the PostgreSQL data directory is part of that tree. See Data Volumes & Backup for the full mapping.
./vibe-coder-data/
├── workspace/ # project sources + APKs
├── postgres/ # PostgreSQL data dir
├── 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,conversation_turns. Schema is created/migrated on boot viaSchemaUtils.createMissingTablesAndColumns. -
Cascade: Foreign keys reference
projects.id. PostgreSQL enforces these.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.
-
conversation_turns: stores Claude console turns with a JSONB column fortool_useinput/output and a GIN tsvector for full-text search. -
Audit log:
audit_logtable records IAM-level actions (auth / project / build / MCP / settings / git / console / publish / 2FA / session timeout). See the Audit Log page for the schema and filter URL recipes. -
admin_users.totp_secret+totp_enabled_at— 2FA TOTP secret + enablement timestamp. See Two-Factor Auth. -
build_schedules— cron expression + variant + enabled flag + lastFiredAt. See Build Automation. -
build_webhook_secrets— secret-id + SHA-256 hash + lastUsedAt for external trigger auth.
-
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. 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. Timing-safe dummy verify on missing users.
-
2FA TOTP: when enabled, login requires a 6-digit code
after password. Server returns
401 totp_requiredon first call, expectstotpCodefield on the retry. -
Session idle timeout:
security.sessionIdleTimeoutMinutes(default 30) auto-deletes device rows whoselastSeenAtexceeded the threshold. Enforced both inAuthPlugin(Bearer) and SSRrequireSessionOrRedirect. -
Single-admin: this is a single-operator tool. There is one admin, and
every authenticated session has full access — authentication is the only
access boundary.
WebSession.isAdmin/canWriteandDevicePrincipal.isAdmin/canWriteare alwaystrue.
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.
Notable parts of the wire surface:
- Project registration (
RegisterProjectRequestDto) carries git-clone fields and atemplateId; the env-setup APIs cover SDK / MCP / Claude auth. - Console turn control:
ApiPath.claudeConsoleCancel(projectId). - Conversation history:
GET /projects/{id}/history+GET /chat/history(backed byconversation_turns), plusGET /api/projects/{id}/claude/prompt-suggestions?prefix=...(server-only{"suggestions": [...]}map). - Git write:
ApiPath.gitCommit(projectId). - Prompt templates:
ApiPath.PROMPT_TEMPLATES+PromptTemplateDto/PromptTemplateListResponseDto. - Claude status:
ClaudeStatusDto.usagePercent+resetAt. - Auth:
LoginRequestDto.totpCode; 2FA-enabled accounts signal401 totp_requireduntil the field is supplied. - Build webhooks:
POST /api/webhooks/build/{projectId}external trigger (no admin auth; multi-secret viaX-Vibe-Secret-Id+X-Vibe-Secret+ optionalX-Vibe-Signature). - Sub-agents:
GET /api/agentsBearer JSON (lists~/.claude/agents/*.md).SubAgentSessionManagerruns independent Claude child processes per(projectId, agentName), with 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). Turns persist alongside the main console via theconversation_turns.agent_namecolumn (ConversationHistoryServicetakes anagentName: String?). - Web Push:
WebPushNotifier(VAPID P-256 ECDSA + RFC 8292 JWT;Aes128GcmEncryptpure-JDK RFC 8291aes128gcmcontent-encoding) +PushSubscriptionRepository+/settings/pushSSR +/api/push/{vapid-public-key, subscribe, subscriptions/{id}}. TheNotifiersfacade exposes awebPushchannel; the service worker reads the decryptedevent.data.json()payload (title / body / url) and routes click to the matching open tab. - WebAuthn:
WebauthnService(wrapswebauthn4j0.29.1) +WebauthnCredentialRepository+WebauthnSectionconfig +/webauthnSSR- 4 JSON endpoints (
POST /api/webauthn/{register,assert}/{options,verify}). Theadmin_users.passwordless_onlyflag +/webauthntoggle enable a passkey-only login flow next to password / TOTP (AuthService.login(hasPasskey)callback).
- 4 JSON endpoints (
- Usage viewer:
/usageshows the cached Claude/statusraw output (ClaudeStatusService.rawSnapshots). - History filtering / search:
ConversationTurnRepository.Filter.agentName(3-mode:nullmain only /""all /"<name>"specific) +distinctAgents(projectId). Full-text search uses acontent_tsvGENERATED ALWAYS AS STOREDcolumn + GIN index, withTsvectorMatchOp(privateOp<Boolean>); non-ASCII queries auto-route through apg_trgmGINgin_trgm_opsindex oncontentviaTrigramIlikeOp. - Symbols:
SymbolFinder(Kotlin/Java regex-based definition lookup) +symbolRoutes(GET /projects/{id}/symbolsSSR +GET /api/projects/{id}/symbols?name=). The file viewer reads a?line=Nquery and smooth-scrolls + outlines the target line. - Metrics + rate limit:
MetricsRegistry+/metricsSSR (Prometheus text exposition; zero deps);RateLimiter+installRateLimitKtor plugin (/api/,/ws/,/login; 429 +Retry-After; configsecurity.rateLimit.*). - Build analysis:
BuildService.compareWithPrevious(...)+ comparison card on the build detail page;BuildService.statistics(...)+ builds-list stats card (success rate / avg duration / inline SVG sparkline + APK size trend). - Backup:
BackupService+BackupSchedulercron polling + SSR endpoints/backup/auto/{name},/backup/auto/{name}/delete,/backup/auto/run-now. - Memo / star:
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=). - Usage reporting:
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, and/usageshows a structured cache stats card.
Multi-session General Chat (ChatGPT-style) lets each chat be a ghost project
__chat_<id>__ (alongside the __scratch__ ghost), living under
<root>/.vibecoder/<id> like scratch. It reuses ClaudeSessionManager
(process / session-id / --resume), ConversationTurnRepository
(per-projectId turn isolation) and the WS console topic verbatim — no DB
schema change. SSR routes are POST /chat/new, POST /chat/{id}/rename,
POST /chat/{id}/delete, and GET /chat?c=<id>; the prompt/cancel/new JSON
APIs take the active chat's ghost projectId unchanged (no wire change).
ProjectService.isGhost(id) (scratch + every __chat_*) governs project
listing, dashboard projectCount, vibe_projects_total, and console
redirects. The left sidebar (WebProjectTemplates.chatSidebar) lists chats,
auto-titled from the first user prompt; consolePage takes chatSidebar /
chatTitle params and wraps the console body in a flex shell.
Chat is conversation-only: when WorkspacePath.isGhostId(projectId) (scratch
- every
__chat_*),ClaudeSessionManagerappendsBash Write Edit NotebookEdit Taskto--disallowedTools, so a chat session can't create/modify files, run builds, or dispatch sub-agents — it only emits text.Read/Glob/GrepandWebSearch/WebFetchstay allowed so the model can still read context and search the web. Regular project consoles are unaffected. This applies from the next process (re)spawn; "New session" forces it.
| Service | Polls | Shutdown hook |
|---|---|---|
ClaudeUsageMonitor |
claude /status every 5 min |
yes |
DiskMonitor |
Files.getFileStore(root) every 10 min |
yes |
BuildScheduler |
enabled build_schedules every 60 s |
yes |
ConversationArchiver |
conversation_turns once every 24 h |
yes |
Notifiers (email + webhook + webPush) |
n/a (event-driven) | yes |
ClaudeSessionManager |
per-project child processes | yes |
SubAgentSessionManager |
per (project, agent) child processes; persists turns |
yes |
WebPushNotifier |
n/a (event-driven; lazy VAPID keypair; aes128gcm encrypted) | n/a (JDK HttpClient close on JVM exit) |
WebauthnService |
n/a (per-request; 5 min in-memory challenge TTL) | n/a |
MetricsRegistry |
n/a (sampled on each /metrics scrape) |
n/a |
RateLimiter |
n/a (in-memory per-IP token buckets) | n/a (state lost on restart by design) |
BackupScheduler |
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.