feat(builder): drive builds through the B4P push channel - #6
Draft
ozsay wants to merge 1 commit into
Draft
Conversation
👷 Deploy request for base44-platform-starter pending review.A Netlify team Owner will need to approve the deploy before you can run your build. Are you a team Owner? Visit the deploys page to approve it → Need more help? Learn more in the Netlify docs →
|
ozsay
force-pushed
the
feat/build-session-push-channel
branch
from
August 27, 2026 14:40
13a6be2 to
15b5aab
Compare
The sidebar drove Base44's builder the only way the platform API allowed: `sendMessage` held an HTTP request open for the whole turn (~28s, hence the 120s timeout), progress came from re-reading `getApp` + `getConversation` every 2.5s, and "the build finished" was inferred from a polling edge — an app seen `processing` that stopped being `processing`. There was no way to stop a turn, and an out-of-credits build read as a generic error. Base44's build-session API replaces all four. This wires Sunny to it as the reference implementation. ## Two mechanisms, one switch `NEXT_PUBLIC_BASE44_BUILD_SESSIONS=1` selects the push channel; unset keeps today's blocking-plus-polling path byte-for-byte. Both are kept so they can be demoed back to back, and so this repo still works against a platform host that predates the endpoints. `live` is the only real branch in the sidebar. ## What the browser holds Five ops go through the server proxy — mint a grant, send, respond, cancel, revoke — and then the browser streams **directly** from Base44 over SSE, keeping the stream off Netlify's function path where buffering would break it. State and completion arrive as events; a `message.updated` triggers one debounced `getConversation`, because the stream deliberately carries no tool *results*. So the transcript read still happens, but only when the transcript actually moved. `turn.finished` is a real completion signal, so the edge detector and the ref that remembered which app it belonged to are confined to the poll path. Stop is a button now. Out of credits arrives as `status: blocked, reason: quota` and reads as a pause rather than a failure — the old mechanism could only ever see `status: error`. ## One credential reaches the browser, and only one The build-session *grant*: read-only, ~15 minutes, scoped to a single session id, revocable by id. The vended access token it was minted with never leaves the server. The grant never goes in a URL either — it is exchanged in a header for a single-use 60-second ticket, so what can land in an access log is already spent. `subscribeToSession` also hands each grant back on teardown; a build re-mints every twelve minutes, so that is the difference between one live read credential and one per refresh. This is a genuine amendment to the repo's own rule that vended tokens stay server-side, so `CLAUDE.md` states it rather than leaving it implied. ## Three defects found verifying this against a live preview A send derived its idempotency key from the message text, so sending the same thing twice read as a network retry: the platform held the first turn's claim for ten minutes and answered the second with a 202 that ran nothing. Sends now carry an opaque per-click id; only a *respond* keeps a key derived from its waitpoint, which is where retry-safety is wanted. `conversation.reset` and `files.changed` were ignored, so a checkpoint restore left the transcript disagreeing with the server. Both re-read now. `requestRefresh` dropped a refresh whenever one was in flight — survivable for `message.updated`, where another event follows, permanent for `conversation.reset`, which is the only notice there is. It re-arms instead. ## One host again `IDENTITY_BASE44_PLATFORM_HOST` and `SSE_BASE44_PLATFORM_HOST` existed only because the two halves of this integration sat on unmerged branches and had to be aimed at different deploy previews. Both have landed, so the split and the per-action `host` override in the proxy are gone. ## Also here `docker-compose.yml` (local Postgres) and `scripts/dev-session.ts` (mint a local session cookie without a Google client) are the harness this was developed against. Unrelated to the push channel and easy to split out. Verified: `typecheck` and `lint` clean, `session:smoke` 29/29, `base44:smoke`, `rls:smoke`, `auth:smoke`, `entities:smoke` all pass. `sunny:smoke` and `market:smoke` fail on this branch's base too — the local database predates the two marketplace migrations.
ozsay
force-pushed
the
feat/build-session-push-channel
branch
from
August 27, 2026 14:48
15b5aab to
6dc08f3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wires Sunny to Base44's build-session API — the partner-facing push channel from apper#21516 — as the reference implementation a partner copies.
Why
The sidebar drove the builder the only way the platform API allowed, and every part of it was a workaround:
sendMessageholds the request open for the whole turn (~28s, henceBUILDER_TIMEOUT_MS = 120s)getApp+getConversationevery 2.5s; the transcript grows all build long, so reads queue behind each other until the proxy times outprocessingthat stops beingprocessingturn.finishedstatus: blocked, reason: quota, rendered as a pauseThe switch
NEXT_PUBLIC_BASE44_BUILD_SESSIONS=1selects the push channel. Unset keeps the old path byte-for-byte, so this repo still works against a platform host that predates the endpoints and the two mechanisms can be demoed back to back.liveis the only real branch inAppBuilderSidebar.Shape
Five ops go through the server proxy (
mintBuildSessionToken,sendBuildSessionMessage,respondToBuildSession,cancelBuildSessionTurn,revokeBuildSessionGrant), then the browser streams directly from Base44 — which keeps SSE off Netlify's function path, where buffering would break it.The stream deliberately carries no tool results, so
message.updatedtriggers one debouncedgetConversation. The transcript read still happens; it just no longer happens on a timer.One credential reaches the browser, and only one
The build-session grant: read-only, ~15 minutes, scoped to a single session id, revocable by id. The vended access token it was minted with never leaves the server. The grant never goes in a URL either — it is exchanged in a header for a single-use 60-second ticket, so what can appear in an access log is already spent.
subscribeToSessionhands each grant back on teardown; a build re-mints every twelve minutes, so that is the difference between one live read credential and one per refresh.This is a real amendment to this repo's rule that vended tokens stay server-side, so
CLAUDE.mdnow states it — including why it does not generalise to "tokens may reach the browser."Three defects, found verifying against a live preview
conversation.resetandfiles.changedwere ignored, so a checkpoint restore left the transcript disagreeing with the server.requestRefreshdropped a refresh whenever one was in flight — survivable formessage.updated, where another event follows; permanent forconversation.reset, which is the only notice there is. It re-arms instead of dropping.One host again
IDENTITY_BASE44_PLATFORM_HOSTandSSE_BASE44_PLATFORM_HOSTexisted only because the two halves of this integration sat on unmerged branches that had to be aimed at different deploy previews. Both have landed, so the two vars, theidentityHost/buildSessionHosthelpers, and the per-actionhostoverride in the proxy are all gone.Rebased onto this repo
The original branch was written against
NitzansiPublic/base44-platform-starter, which shares no history with this repo, so this is a replay rather than a merge. 18 of the files applied unchanged;AppBuilderSidebar.jsxwas re-applied by hand onto the marketplace work, and two hunks were dropped because this repo had already solved the same problems better:setAppReadyForis gone here in favour of a derivedappReady— which fixes the same "only the client that watched the build finish sees it" bug theturn.finishedhandler was written for.onTurnFinishednow just pulls the transcript.seenCommitRefalready re-resolves an open preview when the commit moves, andappRefresh.tsalready broadcasts to every embedded copy.files.changedjust requests a refresh and lets those fire.The marketplace publish paths all use an awaited
refresh(), so switching the poll off does not affect them.Also included
docker-compose.yml(local Postgres) andscripts/dev-session.ts(mint a local session cookie without a Google OAuth client) are the harness this was developed against. Unrelated to the push channel — say the word and I will split them out.Verification
npm run typecheck— cleannpm run lint— clean (2 pre-existing warnings inviews/Board*.jsx, untouched here)npm run session:smoke— 29/29, new suite covering the trigger allow-list, thedecision-vs-actionwire-name trap, thequotakind's deliberate absence, and the fresh-key regressionnpm run base44:smoke,rls:smoke,auth:smoke,entities:smoke— passsunny:smoke/market:smokefail on this branch's base too: the local database has 2 of 4 migrations applied and predatesmarketplace_listings/app_installsStill needs a linked account with credits to exercise end to end: a real interrupt widget resuming via
/responses, Stop mid-build, sending the same message twice (both turns must run), and a checkpoint restore re-reading the transcript. The manual checklist is at the bottom ofscripts/build-session-smoke.ts.