Skip to content

REST API Reference

Sia edited this page Jun 9, 2026 · 24 revisions

REST API Reference

Complete endpoint catalog. All routes are defined as constants in shared/.../ApiPath.kt; copy them into your client to stay wire-compatible.

Authentication

All /api/* routes (except /api/auth/setup, /api/auth/setup/status, /api/auth/login, and /health) require Bearer authentication:

Authorization: Bearer <token>

Obtain a token with POST /api/auth/login. The same token works with both the header and the vibe_session cookie path.

Base URL

http://<host>:17880

For LAN deployments. HTTPS is the operator's responsibility (reverse proxy recommended for non-LAN exposure).


Auth

POST /api/auth/setup/status

Check whether an admin user exists. Used by clients to decide between login and setup screen.

curl http://localhost:17880/api/auth/setup/status
# → {"adminExists": true}

POST /api/auth/setup

Create the first admin user (only when no admin exists). Returns 409 Conflict if one already exists.

curl -X POST http://localhost:17880/api/auth/setup \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe123","deviceName":"my-laptop"}'
# → {"token":"...","deviceId":"...","serverName":"...","username":"admin"}

POST /api/auth/login

curl -X POST http://localhost:17880/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe123","deviceName":"my-android"}'
# → {"token":"...","deviceId":"...","serverName":"...","username":"admin"}

10 consecutive failures lock the account for 15 minutes (timing-safe).

Two-factor authentication

If the user has TOTP enabled (see Two-Factor Auth) the first call without totpCode returns:

HTTP/1.1 401 Unauthorized
{"code":"totp_required","message":"2단계 인증 코드가 필요합니다."}

Client should prompt the user for the 6-digit code from their Authenticator app and resubmit with totpCode added:

curl -X POST http://localhost:17880/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe123","deviceName":"my-android","totpCode":"123456"}'

Wrong code returns 401 invalid_totp (counts against the brute-force limits). totp_required does not (1단계 통과 = legitimate progress).

Session timeout

The bearer token / vibe_session cookie auto-expires after security.sessionIdleTimeoutMinutes (default 30) of inactivity. SSR redirects to /login?err=session_timeout; JSON API returns plain 401.

POST /api/auth/password

Change password. Requires Bearer auth.

curl -X POST http://localhost:17880/api/auth/password \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"currentPassword":"old","newPassword":"new"}'
# → 204 No Content

Server status

GET /api/server/status

High-level summary.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/server/status
{
  "serverName": "Vibe Coder Server",
  "serverVersion": "0.10.0",
  "osName": "Linux 6.x",
  "javaVersion": "17.0.19",
  "workspaceRoot": "/workspace",
  "projectCount": 3,
  "runningTaskCount": 0,
  "claudeAvailable": true,
  "androidSdkAvailable": true,
  "gitAvailable": true,
  "freeDiskSpaceBytes": 102400000000
}

GET /api/server/environment / GET /api/server/environment/check

Detailed per-component diagnostics.


Projects

GET /api/projects

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/projects
# → [{"id":"my-app","name":"My App","packageName":"...", ...}]

POST /api/projects/register

Empty project:

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp"
  }'

Clone from git:

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp",
    "sourceType": "clone",
    "cloneUrl": "https://github.com/owner/repo.git",
    "cloneBranch": "main"
  }'

Private SSH URL — first register a PAT or generate an SSH key via the Git integrations endpoints below.

Optional templateId field

When neither sourceType=clone nor an uploadedZipId is provided, the server can scaffold the project from a built-in template instead of leaving an empty directory.

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp",
    "templateId": "compose-mvvm-hilt"
  }'

Template IDs (resolved by ProjectTemplates.byId(...)):

templateId Description
empty Bare CLAUDE.md only (default)
compose-basic Single-activity Compose Material3
compose-mvvm-hilt ViewModel + Hilt skeleton
compose-mvvm-room Hilt + Room database wired
wear-os Wear OS Compose entry point
android-tv Leanback / TV-launcher entry

Each template seeds a starterPrompt that the project's first console turn consumes — so the next user prompt feels like a follow-up to a real conversation. The user can override or ignore it.

Optional keystore field (JSON API only)

RegisterProjectRequestDto.keystore is accepted by the server but is not exposed in the admin SSR project-create form — it is reachable only via this JSON endpoint (curl / mobile client / direct API).

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp",
    "keystore": {
      "alias": "myapp",
      "password": "at-least-6-chars",
      "dname": "CN=Jangwook Lee, OU=Mobile, O=Sia Makerlab, L=Jecheon, ST=Chungbuk, C=KR",
      "validityDays": 36500
    }
  }'

KeystoreRequestDto fields:

Field Type Required Default Notes
alias String Non-blank. Becomes keyAlias in the generated .properties.
password String Used for both storePassword and keyPassword. Minimum 6 chars.
dname String? Sia Makerlab default DName keytool -dname value.
validityDays Int 36500 (~100 yr) keytool -validity.

Output files (always outside the project folder, in the workspace sidecar):

<workspace>/.vibecoder/keystores/<projectId>/
  <projectId>.keystore               ← keytool -genkeypair output
  <projectId>-keystore.properties    ← Gradle-readable signing config

.properties contents:

storeFile=<workspace>/.vibecoder/keystores/<projectId>/<projectId>.keystore
storePassword=<plain-password>
keyAlias=<alias>
keyPassword=<plain-password>

Security caveat (current implementation). KeystoreGenerator invokes keytool -storepass <plain> -keypass <plain> — the password is therefore briefly visible in ps -ef / /proc/<pid>/cmdline on the host while the command runs (~1 s). Acceptable under vibe-coder's single-operator LAN threat model, but if you share the host with anyone, generate the keystore manually outside the container and only drop the .keystore file in.

If keystore.password policy is stricter than the 6-char minimum, generate the keystore separately and place the files under <workspace>/.vibecoder/keystores/<projectId>/ yourself.

To skip keystore generation entirely, omit the field or send "keystore": null (current default).

POST /api/projects/{id}/rename

Rename the project's display name (folder id and package name are unchanged). Body ProjectRenameRequestDto {name}; returns the updated ProjectDto. (v1.122.0)

curl -X POST http://localhost:17880/api/projects/my-app/rename \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"My Renamed App"}'

DELETE /api/projects/{id}

Delete the project (DB + workspace folder). 204 on success, 404 if absent.


Builds

POST /api/projects/{id}/build/debug

Queue a debug build. Returns immediately with a BuildDto; stream live output via /ws/projects/{id}/builds/{buildId}/logs.

POST /api/projects/{id}/build/release

Queue a release APK build (assembleRelease) with keystore signing injected. Returns BuildDto (202). 409 keystore_required if no keystore matches the project's packageName (policy: no auto-generation — register one via Settings → Keystores first). (v1.118.0)

POST /api/projects/{id}/build/bundle

Queue a release AAB bundle build (bundleRelease) for Play Console upload. Same signing + 409 keystore_required behaviour as build/release. (v1.118.0)

curl -X POST -H "Authorization: Bearer $TOKEN" \
  https://vibe.wody.work/api/projects/myapp/build/release
# → 202 {"id":"…","variant":"release","status":"PENDING",…}

GET /api/projects/{id}/builds

List recent builds.

POST /api/projects/{id}/builds/{buildId}/cancel

Send SIGTERM (then SIGKILL after 5 s) to the build subprocess.

POST /api/projects/{id}/play-upload

Trigger a Google Play upload. Body PlayUploadRequestDto {aabPath?, track?, releaseNotes?} (defaults: standard Release-AAB path, internal track). The server sends a Claude console prompt to upload the AAB via the google-play-publisher MCP — progress streams over the console WebSocket. Returns StoreUploadResponseDto {ok} (202). (v1.121.0)

GET /api/projects/{id}/artifacts/{artifactId}/download

Stream the produced APK.


Quality (Android Lint) (v1.119.0)

Static analysis only — the instrumented-test (connectedDebugAndroidTest) half of the SSR /projects/{id}/quality tab needs an emulator and is not exposed over JSON.

POST /api/projects/{id}/quality/lint?module=app

Run :module:lintDebug and return LintResultDto ({ok, moduleName, durationMs, issues:[{id,category,severity,message,file,line}], errorMessage?, rawTail?}). Synchronous (tens of seconds–minutes). ok=false → lint itself failed (see errorMessage/rawTail). ok=true + empty issues → clean.

POST /api/projects/{id}/quality/fix

Body QualityFixRequestDto {module, kind:"lint"|"test", selected:[String]}. Sends the selected issues to the console (Claude session) as a fix-request prompt; returns {sent}. Progress flows over the console WebSocket.


Project archive (v1.119.0)

JSON variant of the SSR /archive page. The instrumented-test exclusion does not apply here — archive is purely server file ops + DB.

GET /api/archives

List archived projects → [ArchivedProjectDto] ({id, originalId, name, packageName, archivedAt, sizeBytes}).

POST /api/projects/{id}/archive

Compress (.tar.gz) + remove the project, registering the archive. Idle-guarded (same as folder rename) — 409 project_busy while a turn/build/automation runs. Returns ArchivedProjectDto (202).

POST /api/archives/{aid}/restore

Restore back to the original projectId.

DELETE /api/archives/{aid}

Permanently delete the archive (file + registry row).

GET /api/archives/{aid}/download

Stream the .tar.gz (attachment).


Claude Console (per-project persistent session)

POST /api/projects/{id}/claude/console/prompt

curl -X POST http://localhost:17880/api/projects/my-app/claude/console/prompt \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"Add a settings screen with a dark mode toggle"}'
# → 202 Accepted (response streamed over WebSocket)

POST /api/projects/{id}/claude/console/new

Tear down the current session and start fresh (--resume <id> is dropped).

POST /api/projects/{id}/claude/console/cancel

Send SIGTERM to the running Claude child process and preserve the saved session-id. The next prompt resumes the same conversation via --resume. Use to abort a turn that's heading in the wrong direction.

curl -X POST http://localhost:17880/api/projects/my-app/claude/console/cancel \
  -H "Authorization: Bearer $TOKEN"
# → 202 Accepted (no body)

GET /api/projects/{id}/claude/status

Model, plan, quota remaining (cached 60 s).

{
  "sessionId": "01J...",
  "processAlive": true,
  "model": "claude-opus-4-7-1m",
  "plan": "Max",
  "quotaRemaining": "20% remaining, resets at 2026-05-25T00:00Z",
  "usagePercent": 80,
  "resetAt": "resets at 2026-05-25T00:00Z",
  "updatedAt": "2026-05-24T15:00:00Z"
}

usagePercent and resetAt are best-effort (parsed from the CLI's /status plain-text output). Older CLI versions or unfamiliar output formats yield null, in which case the dashboard card gracefully shows "couldn't parse" rather than misleading numbers.

The same value powers the Email Notifications + Webhook Notifications threshold alerts, fired on transition past warnThresholdPercent (default 80) or criticalThresholdPercent (default 95).


Prompt templates

GET /api/prompt-templates

Returns the full user-defined prompt library. The browser console page uses this to populate the ▼ dropdown. Android client may use the same to ship a "templates" picker.

curl http://localhost:17880/api/prompt-templates \
  -H "Authorization: Bearer $TOKEN"
{
  "templates": [
    {
      "id": "t-7a3...",
      "title": "Add a Compose Settings screen",
      "category": "Android",
      "body": "Add a Settings screen using Compose Material3 with a dark-mode Switch...",
      "createdAt": "2026-05-23T12:00:00Z",
      "updatedAt": "2026-05-24T08:11:30Z"
    }
  ]
}

CRUD lives behind the SSR /prompts page. The store is a single JSON file at <workspace>/.vibecoder/prompt-templates.json (RW lock).


Audit log (UI only)

/audit is SSR. For direct querying see the Audit Log page's psql snippet.


Conversation history

Every assistant message + user prompt + tool invocation is persisted to the conversation_turns table for both per-project sessions and the General Chat scratch project.

GET /api/projects/{projectId}/history

curl -H "Authorization: Bearer $TOKEN" \
  'http://localhost:17880/api/projects/my-app/history?limit=50&sessionId=...'
{
  "turns": [
    {
      "id": 1234,
      "sessionId": "01J...",
      "turnIdx": 0,
      "ts": "2026-05-24T10:00:00Z",
      "role": "user",
      "content": "Add a settings screen with dark-mode toggle.",
      "toolName": null,
      "toolUseId": null,
      "tokensIn": null,
      "tokensOut": null
    },
    {
      "id": 1235,
      "sessionId": "01J...",
      "turnIdx": 1,
      "ts": "2026-05-24T10:00:08Z",
      "role": "assistant",
      "content": "I'll add a Settings composable with a Switch wired to DataStore.",
      "toolName": null,
      "toolUseId": null,
      "tokensIn": 712,
      "tokensOut": 134
    }
  ],
  "nextCursor": null
}

Query parameters: limit (default 100, max 500), sessionId (filter to a single Claude session), before (cursor for pagination — pass the turns[0].id from the previous page).

GET /api/chat/history

Same shape as above for the General Chat scratch project (__scratch__). No projectId path segment.

See Conversation History for the page UI and retention strategy.


Memos (global / per-project)

v1.91.0+. Free-form memos, separate from the turn-inline conversation_turns.user_memo (v0.61.0). A memo with projectId: null is global (shown on every project screen); a non-null projectId scopes it to that project. Bearer token or session cookie auth; CSRF is not required (same policy as other /api Bearer routes). POST/PUT/ DELETE require write permission.

GET /api/memos

List all memos, or — with ?projectId=X — global memos plus project X's memos (the exact set the project screen's rail widget shows). Sorted by updatedAt descending.

# all memos (sidebar /memos page)
curl -H "Authorization: Bearer $TOKEN" \
  'http://localhost:17880/api/memos'

# global + project "my-app" (project rail widget)
curl -H "Authorization: Bearer $TOKEN" \
  'http://localhost:17880/api/memos?projectId=my-app'
{
  "memos": [
    {
      "id": "task-20260602-143022-001",
      "projectId": null,
      "content": "Remember to bump versionCode before release.",
      "createdAt": "2026-06-02T14:30:22+09:00",
      "updatedAt": "2026-06-02T14:30:22+09:00"
    },
    {
      "id": "task-20260602-150110-002",
      "projectId": "my-app",
      "content": "Settings screen still needs the dark-mode persistence test.",
      "createdAt": "2026-06-02T15:01:10+09:00",
      "updatedAt": "2026-06-02T15:01:10+09:00"
    }
  ]
}

POST /api/memos

Create a memo. projectId null/omitted → global.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"projectId":"my-app","content":"Wire the Switch to DataStore."}' \
  'http://localhost:17880/api/memos'
# → 200 {"id":"...","projectId":"my-app","content":"...","createdAt":"...","updatedAt":"..."}

PUT /api/memos/{memoId}

Update body and/or scope. keepScope: true (default) keeps the existing projectId and updates content only; keepScope: false moves the memo to projectId (null = global).

curl -X PUT -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"content":"Updated note.","projectId":null,"keepScope":false}' \
  'http://localhost:17880/api/memos/task-20260602-150110-002'
# → 200 (the updated MemoDto)

DELETE /api/memos/{memoId}

curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  'http://localhost:17880/api/memos/task-20260602-150110-002'
# → 200 {"ok":true}

Deleting a project deletes its project-scoped memos (global memos are kept). Renaming a project id repoints its memos.


Git push

Read-only status / diff / log endpoints are joined by a single write endpoint: commit + push.

POST /api/projects/{projectId}/git/commit

curl -X POST http://localhost:17880/api/projects/my-app/git/commit \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "message": "feat: settings screen with dark mode",
    "push": true,
    "onlyTracked": false
  }'
{
  "committed": true,
  "pushed": true,
  "sha": "a1b2c3d4...",
  "branch": "main",
  "errorMessage": null
}

Behavior (sequential, fail-fast):

  1. git config user.name vibe-coder / user.email vibe-coder@localhost (only if unset locally).
  2. git add -A (or git add -u when onlyTracked=true).
  3. git diff --cached --quiet — if nothing staged, returns committed=false, pushed=false.
  4. git commit -m <message> with GIT_AUTHOR_* env.
  5. If push=true, git push <upstream-branch> with GIT_TERMINAL_PROMPT=0 and GIT_SSH_COMMAND='ssh -o BatchMode=yes' — no interactive prompts ever appear.
  6. Push failure does not roll back the commit — returns committed=true, pushed=false, errorMessage=.... Retry with the same call is safe.

Auth is the project's existing PAT (~/.git-credentials) or SSH key (~/.ssh/id_ed25519) — register via the Git integrations endpoints in the previous section.

See Git Workflow for the SSR form.


Project source zip

GET /projects/{id}/zip

Stream the project's source tree as a application/zip. The response uses Content-Disposition: attachment; filename="<projectId>-source-<yyyyMMdd-HHmm>.zip" so browsers immediately download.

curl -L -OJ -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/projects/my-app/zip

Excludes (hard-coded for deterministic backups):

  • .git/, .idea/
  • build/, .gradle/, node_modules/ (anywhere in the tree)
  • *.apk, *.aab

This is a SSR route, not under /api/* — same session-cookie / Bearer auth applies. Streamed directly (no temp file), so large source trees are memory-safe.

See Disk Monitor §"directory-level size" for inverse operation (measuring how much you'd be downloading).


Publishing (SSR only)

Build detail page has CSRF-protected forms that fire a structured prompt into the project's Claude session — actual upload runs through the respective MCP. No REST-level wire today.

Form Endpoint MCP it delegates to
Play Console upload POST /projects/{id}/builds/{buildId}/play-upload google-play-publisher
TestFlight upload POST /projects/{id}/builds/{buildId}/testflight-upload app-store-connect

See Publishing to Stores for the full flow, prechecks, and tested-tester-group caveats.


Emulator AVD lifecycle

There are no emulator endpoints. To test on a real device use wireless ADB (see Android Emulator).


2FA TOTP management (SSR only)

GET /2fa — show enable / disable page.

Form Endpoint
Enable POST /2fa/enable (form field code)
Disable POST /2fa/disable (form field code)

The pending secret lives in server memory until the user verifies a code. See Two-Factor Auth for the threat model and recovery.


Prompt suggestions

GET /api/projects/{projectId}/claude/prompt-suggestions

LIKE-prefix match against this project's user turns. Used by the browser console (and the Android client) for autocomplete.

curl -H "Authorization: Bearer $TOKEN" \
  'http://localhost:17880/api/projects/my-app/claude/prompt-suggestions?prefix=Add%20a&limit=8'
{ "suggestions": [
  "Add a settings screen with dark mode toggle",
  "Add a unit test for SignInViewModel",
  "Add a release-notes section to CHANGELOG"
] }
Query param Default Notes
prefix (required, ≥ 2 chars) LIKE-escaped; sub-2-char ignored to avoid empty-prefix dumps
limit 8 Clamped to [1, 20]

60-second in-memory cache per (projectId, prefix) to absorb keystroke bursts. No persisted DTO in shared/ yet — the JSON shape is the simple {"suggestions": [...]} map above.


Conversation export / import (SSR)

GET /projects/{projectId}/history/export

Streams a JSON envelope of every persisted turn in the project. Content-Disposition: attachment; filename="<id>-conversation-<yyyyMMdd-HHmm>.json".

Envelope (schemaVersion 1):

{
  "schemaVersion": 1,
  "projectId": "my-app",
  "exportedAt": "2026-05-24T15:00:00Z",
  "turnCount": 1234,
  "turns": [
    { "sessionId": "...", "turnIdx": 0, "ts": "...", "role": "user", "content": "...", ... }
  ]
}

POST /projects/{projectId}/history/import

Multipart upload. Query string carries ?_csrf=<session-csrf> because the form is enctype=multipart/form-data and Ktor requireCsrf() reads form fields by default.

Query param Default Notes
_csrf (required) Session CSRF token (sess.csrf)
dryRun true Counts what would be inserted without writing

5 MB body cap. Idempotency is session-id level — sessions already present in the target are skipped wholesale.

See Conversation Search & Archive for the full flow including auto-archive.


Build automation (SSR + 1 external endpoint)

SSR management — /projects/{id}/automation

Form Endpoint
Add schedule POST /projects/{id}/automation/schedules (form: cronExpr, description)
Toggle schedule POST /projects/{id}/automation/schedules/{scheduleId}/toggle (form: enabled)
Delete schedule POST /projects/{id}/automation/schedules/{scheduleId}/delete
Create webhook secret POST /projects/{id}/automation/secrets (form: name)
Delete webhook secret POST /projects/{id}/automation/secrets/{secretId}/delete

External trigger — POST /api/webhooks/build/{projectId}

Admin-auth-free endpoint that lets external systems (GitHub Actions, GitLab CI, monitoring) fire a debug build.

SECRET='<paste>'                          # one-time shown in /automation
SECRET_ID='<from-secret-table>'
BODY='{"ci":"github-actions","sha":"deadbeef"}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST "http://<host>:17880/api/webhooks/build/my-app" \
  -H "X-Vibe-Secret-Id: $SECRET_ID" \
  -H "X-Vibe-Secret: $SECRET" \
  -H "X-Vibe-Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  --data "$BODY"
Header Required Purpose
X-Vibe-Secret-Id Row lookup in build_webhook_secrets
X-Vibe-Secret Plaintext secret; server compares SHA-256(secret) to stored hash (TLS expected)
X-Vibe-Signature optional HMAC-SHA256(secret, body) hex; if present and body non-empty, also verified

Response: 202 Accepted {"projectId":"my-app","triggered":true}.

See Build Automation for the full security discussion.


Backup (SSR)

GET /backup — page with subdirectory sizes and the download button.

GET /backup/download — streams application/gzip (Content-Disposition vibe-workspace-<yyyyMMdd-HHmm>.tar.gz). Excludes postgres/, dev-tools/gradle/caches+daemon, npm-cache, playwright, .vibecoder/__scratch__/, .vibecoder/*/logs/.

PostgreSQL is not in the tar — use pg_dump separately (the page renders the command). See Backup & Restore.


Cross-project tools (SSR)

  • /history — global conversation search (LIKE + role filter, 200-hit cap, ±100 char excerpt with <mark> highlight).
  • /logs — build-log grep across .vibecoder/<projectId>/logs/*.log (last 2 MB per file scanned, 200-match cap, optional project filter).
  • /projects/{id}/deps — Gradle dependency tree (90 s timeout, raw output 200 KB cap) + group:name:version coordinate extraction.
  • /projects/{id}/env-files — whitelist editor for 7 env / build property files (256 KB cap each, atomic write).
  • /projects/{id}/wrapper — Gradle wrapper version + 1-click upgrade (atomic distributionUrl rewrite).
  • /projects/{id}/stats — file / LoC / size per language (35+ languages classified).
  • /code-search — workspace-wide grep with <mark> highlight (200 match cap, 5 MB / file cap).
  • /multi-console?projects=id1,id2,… — N-pane iframe grid (≤ 6 projects).

All SSR-only (no REST wire).


Agents JSON

GET /api/agents

Bearer-authenticated JSON of the registered Claude sub-agents (from ~/.claude/agents/*.md). Used by the browser console UI's dispatch dropdown; Android / VS Code clients can consume the same.

curl -H "Authorization: Bearer $TOKEN" http://localhost:17880/api/agents
{
  "agents": [
    {
      "name": "code-reviewer",
      "sizeBytes": 1234,
      "preview": "You are a security-focused code reviewer..."
    }
  ]
}

Up to 200 chars of preview per agent. CRUD on the agent files themselves is SSR-only — see Custom Agents.


User management

vibe-coder-server is a single-operator tool: one admin, every authenticated session has full access. There are no multi-user, role, or project-ACL endpoints. See Security Model.


noVNC reverse proxy

There is no emulator VNC reverse proxy. For real-device access use wireless ADB (HOST network for mDNS discovery — docker/compose.host.yml).


Sub-agent process pool

Real multi-agent. Each (projectId, agentName) gets its own Claude child. See Multi-Agent Sub-Agent Pool for the model.

All routes here require authentication (Bearer or vibe_session cookie).

POST /api/projects/{id}/agents/{agent}/console/prompt

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Audit the auth module for OWASP A01-A03 issues."}' \
  http://localhost:17880/api/projects/myapp/agents/reviewer/console/prompt
{"ok": true}

First prompt to a fresh agent session is auto-prefixed with Use the <agent> sub-agent to on the server side.

POST /api/projects/{id}/agents/{agent}/console/cancel

SIGTERMs the alive child. Session-id is preserved so the next prompt resumes the same conversation.

GET /api/projects/{id}/agents/active

Lists agent names with a live child for this project.

{"projectId": "myapp", "agents": ["reviewer", "frontend"]}

WS /ws/projects/{id}/agents/{agent}/console/logs

Same protocol as /ws/projects/{id}/console/logs — same auth (cookie or Bearer first-frame), same frame types (ConsoleSessionStarted, ConsoleAssistant, ConsoleToolUse, ConsoleToolResult, ConsoleError, ConsoleDone, ConsoleSystem, ConsoleReplayBegin/End), same since= query for replay-from-seq.

Unlike the main console WS, client→server frames are ignored here. Send prompts via the REST endpoint above.


Notifications

Polling-based in-app notifications (build, Claude-task, usage, system). Powers both the Android client and the web In-App Notifications bell. Authenticate via Bearer token or the vibe_session cookie.

GET /api/notifications

Returns the unread list (oldest first) plus the total.

{
  "events": [
    { "id": "01J…", "ts": "2026-06-02T01:23:45Z", "kind": "build.success",
      "title": "✓ Build succeeded — myapp", "body": "buildId: 01J…",
      "deepLink": "projects/myapp/builds/01J…", "projectId": "myapp", "read": false }
  ],
  "unreadTotal": 1
}

kind is one of build.success, build.failed, claude.turn_done, claude.stopped, claude.error, usage.threshold, system.

POST /api/notifications/ack

Acknowledge specific notifications (soft — sets ackedAt, kept for audit).

{ "ids": ["01J…", "01J…"] }

Returns 204 No Content.

POST /api/notifications/ack-all

Acknowledge all unread notifications for the caller (the bell's "Clear all" button). No request body. Returns 204 No Content.


Web Push subscriptions

VAPID-authenticated browser notifications. See Web Push for the full lifecycle and limitations (payload-less).

GET /api/push/vapid-public-key

curl -H "Authorization: Bearer $TOKEN" http://localhost:17880/api/push/vapid-public-key
{"publicKey": "BL2...65-byte-uncompressed-base64url"}

The browser uses this as applicationServerKey for PushManager.subscribe.

POST /api/push/subscribe

Upserts a subscription by endpoint (unique). Requires authentication.

{
  "endpoint":  "https://fcm.googleapis.com/fcm/send/cXl…",
  "p256dh":    "BAR…",
  "auth":      "uF1…",
  "userAgent": "Mozilla/5.0 (…)"
}
{"id": "5d6c8b2e-…", "ok": true}

p256dh / auth are kept in the row for forward compatibility with RFC 8291 payload encryption (not yet implemented).

DELETE /api/push/subscriptions/{id}

Removes one row. Browsers should call this on unsubscribe(). The server also auto-removes rows when the push service returns 404 / 410.


WebAuthn / passkey

See WebAuthn (Passkey) for the full lifecycle.

Registration

Both endpoints require an active session (cookie or Bearer).

# 1) Server-side challenge
curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/webauthn/register/options
{
  "challenge": "base64url-32-bytes",
  "rpId": "vibe.local",
  "rpName": "Vibe Coder",
  "userId": "base64url(userId)",
  "username": "alice",
  "excludeCredentialIds": ["base64url-credential-id", ...]
}
# 2) Submit the AuthenticatorAttestationResponse the browser produced
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "clientDataJSON":"base64url",
    "attestationObject":"base64url",
    "transports":["usb","nfc"],
    "name":"MacBook Touch ID"
  }' \
  http://localhost:17880/api/webauthn/register/verify
{ "id": "<row-id>", "name": "MacBook Touch ID", "ok": true }

Assertion (login)

These endpoints are unauthenticated — they ARE the login flow.

# 1) Lookup which credential IDs are registered for the username
curl -X POST -H "Content-Type: application/json" \
  -d '{"username":"alice"}' \
  http://localhost:17880/api/webauthn/assert/options
{
  "challenge": "base64url-32-bytes",
  "rpId": "vibe.local",
  "allowCredentialIds": ["base64url-id", ...]
}
# 2) Submit the AuthenticatorAssertionResponse
curl -X POST -H "Content-Type: application/json" \
  -d '{
    "credentialId":"base64url",
    "authenticatorData":"base64url",
    "clientDataJSON":"base64url",
    "signature":"base64url",
    "userHandle":"base64url-or-null"
  }' \
  http://localhost:17880/api/webauthn/assert/verify

On success the response sets the vibe_session cookie + returns:

{ "token": "<bearer>", "deviceId": "<uuid>", "username": "alice" }

401 Unauthorized on any validation failure (challenge expired, credential unknown, signature invalid, sign-count regression, etc.).


Symbol definition lookup

Best-effort regex scan for Kotlin / Java declarations. See Code Analysis for the full pattern catalog + trade-off discussion.

GET /api/projects/{projectId}/symbols

Bearer-authenticated. Symbol name validated against [A-Za-z_][A-Za-z0-9_]{0,79}; invalid input returns {"hits":[]} (silent — no SQL/regex injection surface).

curl -H "Authorization: Bearer $TOKEN" \
  'http://localhost:17880/api/projects/my-app/symbols?name=onCreate'
{
  "hits": [
    {
      "relPath": "app/src/main/java/.../MainActivity.kt",
      "lineNumber": 24,
      "kind": "fun",
      "line": "override fun onCreate(savedInstanceState: Bundle?) {"
    }
  ]
}

kind is one of fun / class / object / interface / val / var / typealias. Hits are capped at 100 per call; the hint line is clipped at 400 chars.

The browser file viewer accepts a &line=N query parameter that scrolls the rendered code to the matching line and flashes a yellow outline for 1.5 s — clicking a hit URL is enough.


PWA static assets

  • GET /static/manifest.json — Web App Manifest (standalone display, 512x512 icon, theme color #0b0d12).
  • GET /static/sw.js — service worker. Cache-first for /static/*, network-only for /api/* / /ws/* / /admin/*.

Registered automatically by AdminTemplates.shell via inline <script> (navigator.serviceWorker.register('/static/sw.js')).

See PWA & VS Code extension.


Environment setup

GET /api/env-setup/components

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/env-setup/components
{
  "components": [
    {"id":"java","displayName":"JDK 17","status":"INSTALLED","installable":false, ...},
    {"id":"android-sdk","displayName":"Android SDK","status":"MISSING","installable":true, ...},
    {"id":"claude-auth","displayName":"Claude 로그인","status":"INSTALLED","installable":false, ...}
  ]
}

POST /api/env-setup/install-all

Trigger sequential install of every installable component. Returns {"taskId":"..."}. Subscribe to /ws/env-setup/{taskId}/logs for live output.

POST /api/env-setup/{componentId}/install

Install a specific component (e.g. android-sdk).

Tracking install progress (client-side only)

The ComponentStateDto returned by GET /api/env-setup/components has no installingTaskId (or equivalent) field. Its full shape is:

id, displayName, description, sizeHint, status, message, installable

That means the wire does not tell you which component is currently installing. The contract is:

  1. Client calls POST .../install (or install-all).
  2. Server returns EnvSetupTaskDto{ taskId } once.
  3. Client stores the (componentId → taskId) mapping itself and subscribes to /ws/env-setup/{taskId}/logs.
  4. When WsFrame.Done arrives, the client drops the mapping and may refresh GET /components to confirm the new status.

The server has a lastTask[component] cache internally (used by the SSR template to render "View live progress →" links), but it is intentionally not exposed in any JSON response — different clients can be installing different components concurrently from different sessions, so per-client ownership of the taskId is the right model.

If your UI shows a per-component "installing…" spinner, do it from local state seeded by the install response, not by re-polling /components.


Claude authentication

POST /api/env-setup/claude-auth/upload (multipart)

Upload .credentials.json obtained on another machine.

curl -X POST http://localhost:17880/api/env-setup/claude-auth/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@$HOME/.claude/.credentials.json"
# → {"targetPath":"/home/vibe/.claude/.credentials.json", ...}

POST /api/env-setup/claude-auth/api-key

curl -X POST http://localhost:17880/api/env-setup/claude-auth/api-key \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"apiKey":"sk-ant-..."}'
# → 204 No Content

DELETE /api/env-setup/claude-auth/api-key/delete

Remove the API key (also accepts POST for clients that lack DELETE).


Claude semi-automatic web OAuth

POST /api/env-setup/claude-login/start

Spawn claude auth login wrapped in script -q. Returns the initial state.

curl -X POST http://localhost:17880/api/env-setup/claude-login/start \
  -H "Authorization: Bearer $TOKEN"
{
  "id":"task-...",
  "state":"STARTING",
  "url":null,
  "startedAt":"2026-05-23T12:00:00Z",
  "updatedAt":"2026-05-23T12:00:00Z",
  "errorMessage":null,
  "lastLines":[]
}

Poll /api/env-setup/claude-login/status every 1 s until state becomes AWAITING_CODE (and url is set).

GET /api/env-setup/claude-login/status

Same shape as above. Returns 204 if no session active.

POST /api/env-setup/claude-login/submit

curl -X POST http://localhost:17880/api/env-setup/claude-login/submit \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"code":"abc...#xyz"}'

State → VERIFYING, then DONE or FAILED.

POST /api/env-setup/claude-login/cancel

Destroy the child process. State → CANCELED.


MCP catalog

GET /api/env-setup/mcp

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/env-setup/mcp
{
  "entries": [
    {
      "id":"github",
      "displayName":"GitHub",
      "pkg":"@modelcontextprotocol/server-github",
      "description":"...",
      "category":"Git 호스팅 (GitHub / GitLab / Gitea / Bitbucket)",
      "trust":"VERIFIED",
      "recommended":true,
      "homepage":null,
      "configFields":[
        {"key":"GITHUB_PERSONAL_ACCESS_TOKEN","label":"GitHub PAT","placeholder":"ghp_...","isSecret":true,"required":true,"help":"..."}
      ],
      "status":"NOT_INSTALLED",
      "configValues":{},
      "comingSoon":false
    },
    ...
  ]
}

McpConfigFieldDto reference

The configFields[] entries have a fixed shape with these 8 keys (no type, no default — those concepts don't exist in the catalog):

Key Type Required Default Purpose
key String Stored verbatim as the env var name in .mcp.json (e.g. GITHUB_PERSONAL_ACCESS_TOKEN).
label String Human-readable text for the form label.
placeholder String? null HTML placeholder attribute, e.g. "ghp_...".
isSecret Boolean false Render as <input type="password"> / masked field.
required Boolean true Server rejects install with 400 missing_config if a required field is empty.
help String? null Tooltip / sub-label describing the field.
isFile Boolean false true → render a file picker and call the multipart upload endpoint below.
acceptMime String? null When isFile=true, value for HTML <input accept="...">, e.g. ".json,application/json".

Notes:

  • A field's value type is always String at the wire level. Files go through the multipart upload endpoint and the server returns an absolute path which the client then sends back as the String value.
  • There is no default field in the wire. If a catalog entry needs a sensible default (e.g. --workspace-root /workspace for filesystem), it is baked into argsTemplate in McpCatalog.kt, not exposed to the client.

POST /api/env-setup/mcp/install

curl -X POST http://localhost:17880/api/env-setup/mcp/install \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "selections": {
      "filesystem": {},
      "github": {"GITHUB_PERSONAL_ACCESS_TOKEN":"ghp_..."},
      "context7": {}
    }
  }'
# → {"taskId":"..."}    (subscribe to /ws/env-setup/{taskId}/logs)

POST /api/env-setup/mcp/unregister

Removes entries from .mcp.json. The npm package itself stays on disk.

curl -X POST http://localhost:17880/api/env-setup/mcp/unregister \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"ids":["github","slack"]}'
# → 204 No Content

POST /api/env-setup/mcp/{mcpId}/file/{fieldKey} (multipart)

Upload a secret file (Service Account JSON, Apple .p8, OAuth client.json) for an MCP entry whose configFields[].isFile is true.

curl -X POST http://localhost:17880/api/env-setup/mcp/google-play-publisher/file/GOOGLE_PLAY_SERVICE_ACCOUNT_JSON \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@./play-sa.json"
# → {"path":"/home/vibe/.config/mcp-secrets/google-play-publisher-GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.json"}

Then pass the returned path as the value for that config key in the subsequent POST /api/env-setup/mcp/install:

curl -X POST http://localhost:17880/api/env-setup/mcp/install \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "selections": {
      "google-play-publisher": {
        "GOOGLE_PLAY_SERVICE_ACCOUNT_JSON": "/home/vibe/.config/mcp-secrets/google-play-publisher-GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.json",
        "GOOGLE_PLAY_PACKAGE_NAME": "com.siamakerlab.myapp"
      }
    }
  }'
  • File saved with 0600 permissions in ${CLAUDE_CONFIG_DIR}/mcp-secrets/.
  • 128 KB size cap (normal Service Account / .p8 is a few KB).
  • Filename sanitized — only [A-Za-z0-9._-] in the stored name.
  • Re-upload to replace (atomic move).

Git integrations

GET /api/settings/git-integrations

List registered PATs (masked) + SSH public key.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/settings/git-integrations
{
  "tokens": [
    {"provider":"github","host":"github.com","username":"x-access-token","tokenMasked":"••••••••wxyz","createdAt":"2026-05-23T12:00:00Z","note":null}
  ],
  "sshPublicKey": "ssh-ed25519 AAAA... vibe-coder"
}

POST /api/settings/git-integrations

curl -X POST http://localhost:17880/api/settings/git-integrations \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "provider":"github",
    "host":"github.com",
    "username":"x-access-token",
    "token":"ghp_...",
    "note":"vibe-coder PAT, expires 2026-12"
  }'
# → 204 No Content

POST /api/settings/git-integrations/delete

curl -X POST http://localhost:17880/api/settings/git-integrations/delete \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"host":"github.com"}'

POST /api/settings/git-integrations/ssh-keygen

Generate ed25519 key pair if missing. Returns the full state including the public key for the user to copy.


Error response shape

{
  "code": "missing_clone_url",
  "message": "sourceType=clone 일 때 cloneUrl 이 필수입니다.",
  "detail": null
}

HTTP status codes: 400 bad input, 401 no/invalid token, 403 insufficient permission, 404 not found, 409 conflict, 413 payload too large, 502 upstream failed (git clone, npm install), 504 timeout.

Clone this wiki locally