Skip to content

AppBuilder - Replace polling with SSE real-time preview events#235

Draft
eshurakov wants to merge 3 commits intomainfrom
eshurakov/app-builder-cors-headers
Draft

AppBuilder - Replace polling with SSE real-time preview events#235
eshurakov wants to merge 3 commits intomainfrom
eshurakov/app-builder-cors-headers

Conversation

@eshurakov
Copy link
Contributor

Summary

  • Replace polling-based preview status checks with Server-Sent Events (SSE) for instant feedback on build progress, container lifecycle, and dev server logs
  • Add JWT ticket auth (APP_BUILDER_TICKET_SECRET) so the frontend can connect directly to the worker's SSE endpoint without session cookies
  • Defer push-triggered builds when no user is viewing the preview (onGitPush + pendingBuild flag), automatically building when a user reconnects
  • Add CORS headers for cross-origin requests to preview subdomains

Changes

Worker (cloudflare-app-builder)

  • SSE endpoint GET /apps/{id}/events?ticket=xxx with JWT verification
  • PreviewDO fan-out: eventWriters set, broadcastEvent(), subscribeEvents() with 30s keepalive
  • Events: status, log, error, container-stopped
  • onGitPush(): defers build via pendingBuild persisted flag when eventWriters.size === 0; immediate build otherwise
  • AppBuilderSandbox: subclass with onStop() hook for container sleep detection
  • Removed /build/logs endpoint (merged into /events)

Backend (Next.js)

  • getEventsTicket tRPC query (personal + org) — signs JWT with { userId, projectId }, 5min TTL
  • notifyGitPush() replaces triggerBuild() in GitHub push webhook handler
  • APP_BUILDER_TICKET_SECRET env var

Frontend

  • usePreviewEvents hook: fetch-based SSE parsing, exponential backoff reconnection (max 8 attempts), fresh ticket per reconnect
  • No reconnect after container-stopped — reconnects on tab visibility change only
  • SleepingState and GeneratingState UI components
  • Removed preview-polling.ts and its tests

Auth flow

  1. Frontend calls trpc.appBuilder.getEventsTicket → backend verifies project access, returns signed JWT + worker URL
  2. Frontend opens SSE connection to worker with ticket as query param
  3. Worker verifies JWT signature + expiry, streams events from PreviewDO

Notes

  • APP_BUILDER_TICKET_SECRET must be configured in both the Next.js app and the worker (wrangler secret put)
  • SSE connections don't survive DO hibernation, but the DO stays awake while the sandbox is running

…domains

The keep-alive pings from app.kilo.ai to *.builder.kiloapps.io were
blocked by the browser due to missing CORS headers. This adds
Access-Control-Allow-Origin headers (with preflight support) for
origins specified in the ALLOWED_ORIGINS env var.
- Add SSE endpoint (/apps/{id}/events) with JWT ticket auth for real-time
  preview status, build logs, errors, and container lifecycle events
- Add fan-out architecture in PreviewDO: eventWriters set, broadcastEvent(),
  subscribeEvents() with 30s keepalive to prevent proxy timeout
- Add frontend usePreviewEvents hook with fetch-based SSE parsing,
  exponential backoff reconnection, and fresh ticket on each reconnect
- Add getEventsTicket tRPC query (personal + org) for JWT ticket issuance
- Replace triggerBuild() with onGitPush() for push webhooks: defers build
  when no SSE clients are connected (pendingBuild flag), triggers on reconnect
- Add SleepingState and GeneratingState UI components in AppBuilderPreview
- Add AppBuilderSandbox subclass with onStop() hook for container sleep detection
- Remove preview-polling.ts, its tests, and streamBuildLogs endpoint (merged into /events)
@eshurakov eshurakov marked this pull request as draft February 16, 2026 08:43
// Handle SSE event stream (GET /apps/{app_id}/events)
const eventsMatch = pathname.match(EVENTS_PATTERN);
if (eventsMatch && request.method === 'GET') {
return handleEvents(request, env, eventsMatch[1]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Missing CORS headers for /apps/{appId}/events (browser SSE)

startPreviewEvents() fetches the worker from the browser (cross-origin from app.kilo.ai). This route returns handleEvents(...) without Access-Control-Allow-Origin, so the browser will block the SSE connection.

Consider applying the same allow-list CORS logic here (wrap the response from handleEvents with withCorsHeaders(...) when Origin is allowed), or add the CORS headers inside handleEvents for SSE responses (and error responses) as well.

async () => {
// Fast path: if the container stopped, return 'sleeping' without waking it
if (this.persistedState.containerStopped) {
return { state: 'sleeping', error: this.persistedState.lastError };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Preview state sleeping can break shared schema parsing

getStatus() now returns { state: 'sleeping', ... } when containerStopped is set. The worker’s preview/status endpoint serializes state into JSON, but the shared PreviewStateSchema in cloudflare-app-builder/src/api-schemas.ts does not include sleeping.

Any consumer that parses responses with GetPreviewResponseSchema (e.g. src/lib/app-builder/app-builder-client.ts) will throw on sleeping. Either extend PreviewStateSchema to include sleeping (and propagate to client types) or map sleeping to an existing state at the API boundary.

@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 16, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 0
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

CRITICAL

File Line Issue
cloudflare-app-builder/src/index.ts 168 Missing CORS headers for /apps/{appId}/events SSE response, likely blocks browser connection
cloudflare-app-builder/src/preview-do.ts 433 New preview state sleeping not included in shared PreviewStateSchema, can cause runtime schema-parse failures
Files Reviewed (9 files)
  • cloudflare-app-builder/src/index.ts - 1 issue
  • cloudflare-app-builder/src/preview-do.ts - 1 issue
  • cloudflare-app-builder/src/handlers/events.ts
  • cloudflare-app-builder/src/handlers/preview.ts
  • cloudflare-app-builder/src/app-builder-sandbox.ts
  • cloudflare-app-builder/src/utils/auth.ts
  • cloudflare-app-builder/src/api-schemas.ts
  • src/components/app-builder/project-manager/usePreviewEvents.ts
  • src/components/app-builder/ProjectManager.ts

…wStateSchema

- Add CORS preflight and response headers to /apps/{appId}/events route
  (browser fetches SSE cross-origin from app.kilo.ai)
- Add 'sleeping' to PreviewStateSchema zod enum so client-side parsing
  doesn't reject the new state from getStatus()
- De-duplicate PreviewState: types.ts now re-exports from api-schemas.ts
  instead of maintaining a separate union type
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant