AppBuilder - Replace polling with SSE real-time preview events#235
AppBuilder - Replace polling with SSE real-time preview events#235
Conversation
…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)
cloudflare-app-builder/src/index.ts
Outdated
| // 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]); |
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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.
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)CRITICAL
Files Reviewed (9 files)
|
…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
Summary
APP_BUILDER_TICKET_SECRET) so the frontend can connect directly to the worker's SSE endpoint without session cookiesonGitPush+pendingBuildflag), automatically building when a user reconnectsChanges
Worker (
cloudflare-app-builder)GET /apps/{id}/events?ticket=xxxwith JWT verificationeventWritersset,broadcastEvent(),subscribeEvents()with 30s keepalivestatus,log,error,container-stoppedonGitPush(): defers build viapendingBuildpersisted flag wheneventWriters.size === 0; immediate build otherwiseAppBuilderSandbox: subclass withonStop()hook for container sleep detection/build/logsendpoint (merged into/events)Backend (Next.js)
getEventsTickettRPC query (personal + org) — signs JWT with{ userId, projectId }, 5min TTLnotifyGitPush()replacestriggerBuild()in GitHub push webhook handlerAPP_BUILDER_TICKET_SECRETenv varFrontend
usePreviewEventshook: fetch-based SSE parsing, exponential backoff reconnection (max 8 attempts), fresh ticket per reconnectcontainer-stopped— reconnects on tab visibility change onlySleepingStateandGeneratingStateUI componentspreview-polling.tsand its testsAuth flow
trpc.appBuilder.getEventsTicket→ backend verifies project access, returns signed JWT + worker URLNotes
APP_BUILDER_TICKET_SECRETmust be configured in both the Next.js app and the worker (wrangler secret put)