Skip to content

Web API Reference

Reference: packages/web/

Server (Hono on Bun)

Binding

Bun.serve({ hostname: "127.0.0.1", port: config.port, fetch: app.fetch });

NEVER bind to 0.0.0.0.

LoopManager (singleton)

The server maintains a LoopManager singleton (packages/web/src/server/loop-manager.ts) that:

  • Tracks active loops by project path (max one loop per project)
  • Creates LoopRunner instances from @rauf/loop, subscribes to all 20 event types
  • Fans events out to SSE clients via subscribe(projectPath, listener)
  • Recovers stale loops on startup by scanning discovered projects and resetting stalled in_progress items to pending
  • Handles graceful shutdown (shutdownAll()): cancels all active loops and waits for completion

CSRF Middleware

All POST/PUT/DELETE routes require header: X-Rauf-Request: true. Requests without it get 403 Forbidden. No Access-Control-Allow-Origin headers set (blocks cross-origin).

Static Serving

Built React frontend served from / as static files. API routes under /api/.

API Routes

Health

GET /api/health{ version, uptime, rootDirectory, projectCount }

Projects

GET /api/projects → { data: DiscoveredProject[] }
GET /api/projects/:id → { data: ProjectDetail }
POST /api/projects/:id/install → { data: InstallationReport }
POST /api/projects/init → { data: InstallationReport }
POST /api/projects/:id/update → { data: InstallationReport }
POST /api/projects/:id/uninstall → { data: void }

:id = directory name (URL-encoded). Resolved to ROOT_DIRECTORY/<id>.

Backlog

GET /api/projects/:id/backlog → { data: BacklogItem[] }
Query params: ?status=pending&type=bug&sort=priority
POST /api/projects/:id/backlog → { data: BacklogItem }
Body: CreateItemInput
GET /api/projects/:id/backlog/:itemId → { data: BacklogItem }
PUT /api/projects/:id/backlog/:itemId → { data: BacklogItem }
Body: UpdateItemInput
DELETE /api/projects/:id/backlog/:itemId → { data: void }
POST /api/projects/:id/backlog/restore → { data: void }
POST /api/projects/:id/backlog/sweep → { data: SweepResult }
Body: { minAgeDays?: number }
Note: registered BEFORE /:itemId routes to prevent "sweep" matching as an itemId
GET /api/projects/:id/archive → { data: { months: { month: string, count: number }[] } }
GET /api/projects/:id/archive/:month → { data: ArchiveMonth }
DELETE /api/projects/:id/archive/:month → { data: { purgedCount: number, purgedMonths: string[] } }

Status

GET /api/projects/:id/status → { data: DerivedStatus }
GET /api/projects/:id/log?tail=50 → { data: string[] }
GET /api/projects/:id/log/stream → SSE stream
GET /api/projects/:id/progress → { data: string } (raw markdown)

Profile

GET /api/projects/:id/profile → { data: ProjectProfile }
PUT /api/projects/:id/profile → { data: ProjectProfile }
POST /api/projects/:id/profile/detect → { data: ProjectProfile }

Config

GET /api/config → { data: ToolConfig }
PUT /api/config → { data: ToolConfig }

Loop Management

POST /api/projects/:id/loop/start → { data: { started: true, projectPath } }
Body (optional): { maxIterations?, maxRetries?, model?, sessionTimeoutMinutes?, review?, reviewOnly?, provider? }
Defaults: maxIterations=20, maxRetries=3, sessionTimeoutMinutes=60
409 Conflict: Loop already running for this project
Note (v0.5.0): This route is the backend for `rauf loop run --detached`. URL and contract unchanged.
POST /api/projects/:id/loop/stop → { data: { stopped: true, projectPath } }
404 Not Found: No active loop for this project
GET /api/projects/:id/loop/events → SSE stream of LoopEvent
Event type: "loop_event" (data: JSON-encoded LoopEvent)
Heartbeat: "heartbeat" every 30s (data: ISO timestamp)
Streams until client disconnects

Recovery

All mutation routes require X-Rauf-Request: true header (403 otherwise). An invalid /:id returns 400 INVALID_ID; a path-sandbox violation returns 400 PATH_VIOLATION.

POST /api/projects/:id/reset
Body: { backlogRoot?: string, clearBacklog?: boolean, keepProgress?: boolean, keepLog?: boolean }
Guard: acquires recovery lock (409 LOCK_CONFLICT if a live loop holds the lock)
200: { data: { resetCount, resetIds, treeClean, stalledReset } }
404 if project not installed
POST /api/projects/:id/resume
Body: { backlogRoot?: string, retryBlocked?: boolean, answers?: { itemId: string, text: string }[] }
Guard: acquires recovery lock (409 LOCK_CONFLICT if a live loop holds the lock)
Injects each answer as humanAnswer on the item, optionally unblocks blocked items,
runs recoverInterruptedLoop, then relaunches the loop if there are eligible items.
200: { data: { reconciled: ReconcileSummary, relaunched: boolean, reason?: string } }
A failed relaunch is reported as relaunched:false + reason in a 200 (not an HTTP error).
404 if project not installed
POST /api/projects/:id/loop/review
Body: { model?: string, sessionTimeoutMinutes?: number, backlogRoot?: string }
Guard: loop-start dedupe (409 CONFLICT if a loop/review is already running for this backlog root)
Starts a review-only pass (maxIterations:1, reviewOnly:true).
200: { data: { started: true } }
Note: registered in loop.ts alongside loop/start and loop/stop
POST /api/projects/:id/backlog/unblock
Body: { itemId?: string } — omit itemId to unblock all blocked items
Guard: assertNoLiveLoop (409 LOCK_CONFLICT if a live loop is running; fail-open on lock I/O error)
200: { data: { unblockedCount: number, unblockedIds: string[] } }
Note: registered BEFORE /:itemId routes
GET /api/projects/:id/backlog/validate
Query: ?backlogRoot=<path> (optional; defaults to project's default backlog root)
Guard: none — read-only; no X-Rauf-Request required; safe to call during a live run
200: { data: { valid: boolean, findings: ValidationFinding[] } }
valid:false returns 200 (not an HTTP error); findings list structural issues (e.g. DUPLICATE_ID)
Note: registered BEFORE /:itemId routes

Error Response Format

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Priority must be between 1 and 4",
"details": { "field": "priority", "value": 5 }
}
}

SSE Log Stream

GET /api/projects/:id/log/stream

Event types:

  • log: new log line(s) (data: JSON array of strings)
  • status: loop state change detected (data: DerivedStatus JSON)
  • loop_event: LoopEvent from the loop runner (data: JSON-encoded LoopEvent, see SCHEMAS.md)
  • heartbeat: sent every 30s (data: ISO timestamp)

Implementation:

  • Watch .rauf/rauf.log with fs.watch
  • Poll for new lines every 1s
  • Watch .rauf/state.json for state changes
  • Client auto-reconnects (standard SSE behavior)

Frontend (React SPA)

Router Structure (TanStack Router)

/ → Redirect to /projects
/projects → Projects Dashboard
/projects/:id → Redirect to /projects/:id/backlog
/projects/:id/backlog → Backlog View
/projects/:id/status → Status View
/projects/:id/settings → Project Settings
/install → Installation Wizard
/init → Greenfield Wizard
/settings → Global Settings

Shared Fetch Wrapper

async function raufFetch(url: string, options?: RequestInit) {
return fetch(url, {
...options,
headers: {
...options?.headers,
"X-Rauf-Request": "true",
"Content-Type": "application/json",
},
});
}

All API calls go through this wrapper.

TanStack Query Keys

['projects'] → project list
['projects', id] → project detail
['projects', id, 'backlog'] → backlog items
['projects', id, 'backlog', itemId] → single item
['projects', id, 'status'] → derived status
['projects', id, 'profile'] → project profile
['config'] → tool config
['projects', id, 'loop', 'start'] → start loop mutation
['projects', id, 'loop', 'stop'] → stop loop mutation

UI Views

Projects Dashboard

  • Card grid of discovered projects
  • Each card: name, stack badge, loop state badge, backlog summary, last activity
  • “Install Rauf” and “Initialize New Project” buttons
  • Auto-refresh every 30s + manual refresh button

Backlog View

  • Filter bar: type, status, priority
  • Sort: priority (default), id, status
  • Summary counts row
  • Item cards: ID, type badge, priority, title, status badge, criteria count, dependencies
  • “Add Item” → side panel/modal
  • Click item → edit side panel
  • Active loop warning banner if running

Status View

  • Two-column layout (wide screens)
  • Left: loop state badge, iteration info, backlog summary, current/blocked/recent items
  • Right: live log tail panel (monospaced, SSE-fed, auto-scroll)
  • Loop control buttons: Start Loop (visible when state is IDLE, PAUSED, COMPLETE, or ERROR) / Stop Loop (visible when RUNNING or SLEEPING_LIMIT)
  • Below: progress.md rendered as markdown

Installation Wizard (6 steps)

  1. Select Target (path input, validation)
  2. Preflight (checklist of checks)
  3. Tech Stack & Profile (auto-detected, editable)
  4. Configure (project name, gitignore toggle, options preview)
  5. Review (file list preview, RAUF.md verification section)
  6. Result (installation report, quick links)

Greenfield Wizard (5 steps)

  1. Project Info (name, path, description)
  2. Tech Stack (preset selection, command fields)
  3. Initial Backlog (empty / import file / enter inline)
  4. Review (file preview, CLAUDE.md preview, backlog preview)
  5. Result (creation report, next steps)

Settings

  • ROOT_DIRECTORY path input (triggers re-discovery on change)
  • Server port
  • Theme toggle (light/dark/system)
  • Project visibility toggles

Styling

  • Tailwind CSS (utility-first)
  • No component library dependency; custom components
  • Responsive: works at 1024px+ width
  • Light/dark theme support

Key UX Requirements

  • All destructive actions require confirmation dialog
  • Toast notifications for async results
  • Error boundaries: malformed project files show recovery UI
  • No external network requests (all assets served locally)
  • Markdown rendering via react-markdown + remark-gfm