Skip to content

feat(dev): front the app's dev server with the SDK header contract - #659

Open
netanelgilad wants to merge 4 commits into
mainfrom
claude/inspiring-edison-55ncqj-dev-front-door
Open

netanelgilad wants to merge 4 commits into
mainfrom
claude/inspiring-edison-55ncqj-dev-front-door

Conversation

@netanelgilad

@netanelgilad netanelgilad commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Note

Description

base44 dev becomes the single front door for the whole app, the way the platform serves it in production: the CLI origin answers the platform's surface locally (/api/apps/** for entities, functions, auth, media, plus /api/app-logs/**) and forwards everything else — the app's own /api routes and the HMR websocket included — to the app's own dev server, carrying the SDK header contract (Base44-App-Id, Base44-Api-Url, a locally minted service-role Base44-Service-Authorization) after the caller's copies of those platform-owned headers are dropped. Full-stack apps can therefore call createClientFromRequest locally just as they do in production; previously those headers were absent under base44 dev, so SSR pages and server functions could not build an SDK client at all. On top of that, the front door stands in for the platform's login on a private or workspace app — a page load with no session goes to the CLI's own /login, which signs the developer in as the account the CLI is logged in with, hands the app an access token and keeps it in a session cookie — and base44 dev --remote runs the same front door against the live app's data (and now accepts --port).

Related Issue

None (internal: base44-dev/apper#25003). Split out of #643, which keeps the deploy-routing changes.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

The front door

  • New createAppServerProxy (src/cli/dev/dev-server/routes/app-server.ts): HTTP proxy + websocket relay into app-owned code. Strips the visitor's Base44-App-Id / Base44-Api-Url / Base44-Service-Authorization / Base44-State / Base44-Functions-Version / X-Base44-App-Url / X-Data-Env before setting the CLI's own, so a caller cannot point the app's SDK (and the credential travelling with it) at a host they chose. Authorization passes through untouched — it is the visitor the app's server code acts as.
  • Routing split in main.ts: PLATFORM_PATH_PATTERN (new platform-paths.ts, matching /api/apps and /api/app-logs) stays local; everything else goes to the app dev server. Websocket upgrades follow the same split — REALTIME_PATH (now exported from realtime.ts) keeps the entity-events socket, all other upgrades are relayed.
  • Websockets relayed frame by frame via ws rather than tunnelled: under Bun (the compiled binary), bytes written to a node:http upgrade socket never reach the wire. Client frames sent before the upstream opens are buffered and flushed on open.
  • Loop guard: every forwarded request carries Base44-Dev-Forwarded; seeing it come back answers 508 with the fix (narrow the Vite proxy to /api/apps and /api/app-logs, or update @base44/vite-plugin) instead of looping. Proxy failures answer 502 with the underlying message.
  • App dev server discovery (serve-runner.ts): ServeRunner reads the first http://localhost:<port> the child announces on startup, stripping ANSI first (Vite bolds the port) and skipping the front door's own port, which the Vite plugin echoes as its proxy target. serve-command-runner.ts threads onOrigin / ignorePort through.

Sign-in for private and workspace apps

  • New createLoginGate (src/cli/dev/dev-server/routes/login-gate.ts): a page load (GET, Accept: text/html, outside /api/ and /media/) with no session cookie redirects to /login?from_url=…; the CLI's own page offers "Continue as <the CLI's account>"; submitting it mints an access token, sets the HttpOnly SameSite=Lax base44_dev_session cookie and redirects back with ?access_token=, the hosted login's own hand-off. A cookie-bearing request with no Authorization gets the cookie's token as one, so server-rendered pages act as the visitor. The SDK's /api/apps/auth/logout clears it. from_url is forced back onto the front door's origin so a crafted link cannot carry the token elsewhere, and the values rendered into the page are HTML-escaped.
  • Gating decision: visibility in config.jsonc wins (it is what deploy applies), else the app's public_settings from the server via the new getAppVisibility (core/project/api.ts, AppPublicSettingsSchema); an unreachable server means public, and a public app keeps handling /login itself.
  • Token source: locally a JWT for the account seeded as admin in the local database (createJwtToken); under --remote, the account's app-user token from the platform (getAppUserToken), so the app reads and writes the live app's data as that user.

--remote

  • New createRemoteFrontDoor (remote-front-door.ts): the same single origin with no local backend — the platform's paths and the entity-events socket proxy to the live app, and the app dev server gets Base44-App-Id / Base44-Api-Url but no service-role token (the CLI cannot mint one the live app accepts). --port is now accepted (it picks the front door's port, default 4400) instead of being rejected, and SIGINT/SIGTERM exit the process, since the front door lives in it.

Command, docs and deps

  • dev.ts: wires the origin promise (120s startup timeout with a clear error), passes appServer and loginGate to createDevServer, and the outro now prints Your app is available at <cli url> instead of telling users to open the frontend dev server's URL (--remote adds "it runs on the data of <live app>").
  • Docs: new docs/dev-server.md topic guide, linked from docs/AGENTS.md; CHANGELOG entries under Changed.
  • Deps: ws + @types/ws added as devDependencies (bundled, per the zero-dependency distribution rule).

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

New tests/cli/dev-app-server.spec.ts drives the real dev command against two new fixtures: with-app-server (a node dev server that announces a Vite-shaped, ANSI-bolded URL and echoes what each request arrived with, HMR upgrade included) and with-private-app-server, the same app with "visibility": "private". Eleven cases cover SDK header injection, dropping the caller's copies, the app's own /api/time being forwarded, /api/apps and /api/app-logs staying local, the HMR socket relaying with headers, the entity-events socket staying local, the redirect to the sign-in page, the full sign-in hand-off with the cookie travelling as Authorization on later requests, logout ending the session, and a public app keeping its own /login. dev.spec.ts was updated for --remote now fronting the serve command and accepting --port, and waitForDevServer in the testkit for the new outro wording. The suite was not re-run while writing this description (build and test commands were unavailable in this environment), so the box stays unchecked pending CI on this HEAD.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

User-visible workflow change: open the URL base44 dev prints, not the frontend dev server's own URL. Opening the frontend dev server directly still works for pure-frontend apps but bypasses the header contract and the login gate, so SSR/server-function code will not get an SDK client and a private app will not be signed in.

Since the previous description: /api/app-logs moved back to the platform side of the split (commit 3dd96ab) — the SDK posts client logs there, and forwarding them to the app dev server would take them away from the local backend — and the login gate plus the --remote front door landed in 0f02923.


🤖 Generated by Claude | 2026-10-25 16:38 UTC | 96b3a00

`base44 dev` now serves the whole app on one origin. It answers `/api/apps`
itself and forwards everything else, including the app's own `/api` routes
and its HMR websocket, to the app dev server. Forwarded requests carry
`Base44-App-Id`, `Base44-Api-Url` and a service-role credential, and the
caller's copies of those headers are dropped. Full-stack apps can then build
an SDK client with `createClientFromRequest` locally, as they do in
production.

Websockets are relayed at the frame level through `ws`, because the Bun
compiled binary never writes bytes from a node:http upgrade socket to the
wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ
@netanelgilad netanelgilad self-assigned this Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.2.3-pr.659.96b3a00

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.2.3-pr.659.96b3a00"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.2.3-pr.659.96b3a00"
  }
}

Preview published to npm registry — try new features instantly!

netanelgilad pushed a commit that referenced this pull request Oct 11, 2026
The dev-server front door and its tests now live in #659. This branch
keeps only the deploy routing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ
netanelgilad added a commit that referenced this pull request Oct 21, 2026
…h it (#643)

* feat(dev): front the app's dev server with the SDK header contract

base44 dev now serves the whole app on one origin. /api/apps stays with the
local backend; everything else — the app's own /api routes included — is
forwarded to its dev server carrying Base44-App-Id, Base44-Api-Url and a
service-role credential, with the caller's copies of the platform-owned
headers dropped first. Full-stack apps can build a client with
createClientFromRequest locally, the way they do in production, instead of
500ing on every SSR loader and server function.

The dev server's port is read off the URL it announces at startup (the front
door's own port skipped, since the Vite plugin logs it as its proxy target),
and a request that comes back marked as forwarded answers 508 naming the
Vite proxy rather than looping. Websocket upgrades follow the same split, so
HMR reaches the app and the entity-events socket stays local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* feat(deploy): ship every site through the deployments API, and publish it

`BASE44_DEPLOYMENTS_API` is gone. The lane it gated is now the only transport
`site deploy` has, and `--git-hash` / `--concurrency` are always registered
rather than appearing only when an env var was set. Setting the variable does
nothing; the sandbox that sets it needs no change.

`base44 deploy` ships its site through the same lane instead of the archive
upload, so a full-stack project's worker is no longer silently dropped, and a
build carries the commit it was made at. Because that lane only builds, the
deploy then publishes what it deployed (`--no-publish` opts out) — the archive
upload it replaces always left the app live, and a deploy that quietly stopped
doing so would be a worse default than a flag.

`site deploy` deliberately does NOT publish, and gains `--publish` instead: the
platform's own build step runs it against builder-managed apps and repoints
production itself, so publishing there would fail exactly those apps. When the
server does refuse a publish for that reason (`app_publishes_from_builder`), the
deploy reports it and still succeeds — its resources and its build did land.

The archive upload keeps the two callers that have no commit to be addressed by:
`base44 create`'s scaffold deploy, and a `base44 deploy` in a project with no git
history, which warns and falls back. A worker build has no archive form, so
there a missing commit is fatal.

Depends on base44-dev/apper#26921 for the publish endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* fix(dev): relay the app's websockets through `ws`, so the binary forwards them too

The compiled binary answered every HMR upgrade with an error: under Bun, bytes
written to the socket a node:http `upgrade` listener receives never reach the
wire, so neither http-proxy's tunnel nor a raw-socket pipe can complete a
handshake from it. `ws` is the one path Bun maps onto its native websockets —
it is how Socket.IO's realtime socket already worked there — so the front door
now accepts the browser's socket with it and relays frames to a `ws` client
connected to the dev server, carrying the same SDK headers the HTTP path does.
The two paths now share one `sdkHeaders()` definition.

The HMR test asserted only that the socket opened, which a relay satisfies on
its own, so it now waits for the dev server's own frame and checks what that
frame says the upgrade arrived with. The fixture also tolerates a reset socket,
as a real dev server does: Bun's client resets rather than closing, and the
unhandled error killed the fixture and with it the dev environment.

Adds `ws` (8.21.0, already locked through miniflare) and `@types/ws` as
devDependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* chore: move the `base44 dev` front door to its own PR

The dev-server front door and its tests now live in #659. This branch
keeps only the deploy routing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* feat(deploy): publish a commit through the builder's /deploy

The publish moved server-side (base44-dev/apper#26921): instead of
`POST deployments/{git_hash}/publish`, the CLI calls the builder's own
`POST /deploy` with `{ git_commit_hash }`, then reads the app's URL from
`published-url` (the deploy response carries none). A builder-managed app
now refuses with `error.code` `git_commit_hash_not_supported`, which is
what "production left where it was" keys on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* fix(deploy): don't read the publish response body

`POST /deploy` answers a logged-in user (a `base44 login` token) with the
editor's full app, and only an API key gets the slim record carrying
`git_commit_hash` and `deployed_at`. Requiring those fields failed `base44
deploy` after the publish had already succeeded. The CLI knows the commit
it sent and reads the URL from `published-url`, so it no longer parses the
body. The testkit's `/deploy` mock now answers with the full-app shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* refactor(cli): share one --git-hash parser

`deploy`, `site deploy` and the versions commands each carried the same
argParser; it now lives in `cli/utils/git-hash.ts`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* refactor(cli): drop --no-publish from base44 deploy

It read as covering every resource deploy ships, but only the site waits on a
publish. site deploy keeps --publish as its opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* fix(cli): site deploy says when it didn't publish, and deploys a commitless static build as before

Without a commit, a static build goes through the archive upload and goes live,
as site deploy did before; a worker build still needs one. When it leaves
production alone, it points at --publish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

* refactor(cli): take the deployments API only for an explicit --git-hash

No commit is inferred from the local checkout any more. Without --git-hash,
deploy and site deploy keep the archive upload and go live as they always did;
a full-stack build, which that upload cannot carry, fails asking for the flag,
and deploy checks that before shipping any resource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The SDK's appLogs module calls /api/app-logs/{appId}/..., which is the
platform's, not the app's. The front door only kept /api/apps local, so those
calls went to the app's dev server instead of the backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ
…d --remote

A private or workspace app is behind the platform's login in production. Under
base44 dev, page loads without a session now go to the CLI's own /login, which
signs the developer in as the CLI's account, hands the app an access token the
way the hosted login does (?access_token=), and keeps it in a session cookie
that also identifies the visitor on server-rendered page loads. The SDK's
logout clears it. Public apps keep handling /login themselves.

base44 dev --remote runs the same front door, with the platform's paths
proxied to the live app, so a private app works against live data with the
account's app-user token. It now takes --port, and Ctrl-C exits it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ

Copy link
Copy Markdown
Contributor Author

0f02923 adds sign-in for private and workspace apps under base44 dev and base44 dev --remote. --remote now also runs the front door, with the platform's paths proxied to the live app. Details are in docs/dev-server.md under "Private and Workspace Apps".

End-to-end check. I created a new private app (the apps-examples TanStack guestbook with "visibility": "private"), deployed it, and drove both modes in headless Chromium.

The live app is private: an anonymous GET / returns 401, and an anonymous entity API call returns 403.

Local, base44 dev:

  • / redirects to the CLI's /login, which shows "Continue as netanelg@base44.com".
  • After signing in, the browser lands back on / and the SDK has stored its token.
  • Signing the guestbook writes an entry, and the entry is still there after a reload.
  • Logout returns to /login.

Remote, base44 dev --remote:

  • The same flow passes.
  • The entry it wrote is in the live app's database. I read it back through the app's API with the account's app-user token.
  • Ctrl-C exits the process.

Public app: GET / isn't gated, and /login goes to the app.

Bugs found during the check and fixed in this push:

  • Server-rendered pages called the live private app anonymously and got a 500. The session cookie now carries the token and is sent as Authorization when a request has none, as the platform's cookie does.
  • The live-app proxy had ws: true, which made http-proxy-middleware take every websocket upgrade, so HMR failed with "Invalid frame header".
  • Ctrl-C didn't exit --remote.

Tests:

  • dev-app-server.spec: 4 new gate cases (redirect, sign-in hand-off and cookie identity, logout, public app untouched). 11 of 11 pass.
  • The two dev.spec --remote tests are updated for the new contract (front door URL, --port accepted).
  • Typecheck, lint and knip are clean.
  • The only failures are the two dev.spec cases that also fail on main in this sandbox (no IPv6).

Generated by Claude Code

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.

2 participants