Skip to content

Dev server internals

@jxsuite/server is the reference implementation of the backend protocol : a Bun-native dev server whose /__studio/* mount is the literal route table, plus the proxies and services that make documents behave in development as they do in production. This page is the architecture tour for backend implementers — for using the server day to day, see the dev server in the Framework section.

One handler, an ordered route chain

createDevServer() stands up a single Bun.serve fetch handler that checks routes in a fixed order; the first claimant wins, and anything unclaimed falls through to static file serving:

  1. /__reload — the SSE live-reload endpoint (skipped when watch: false ).

  2. POST /__jx_resolve__ — the $prototype / $src resolution proxy.

  3. POST /__jx_server__ — the timing: "server" function proxy.

  4. /_jx/* — extension server mounts.

  5. /__studio/* — the Studio API (skipped when studio: false ): the collab WebSocket upgrade, activate , then the AI, site-import, and code sub-APIs, then handleStudioApi for everything else.

  6. Custom middleware , if the embedder passed one.

  7. Static files: project files at their natural URLs, the active project's tree, public/ at the site root, and bare npm specifiers resolved through node_modules and bundled on demand.

Ordering is load-bearing: the privileged endpoints must be claimed before the static fallback can ever see their paths, and middleware deliberately sits after the protocol routes so an embedder cannot accidentally shadow them.

The Studio API mount and activation

handleStudioApi (in studio-api.ts ) implements the protocol's route table against the filesystem. Its central piece of state is the active project root : POST /__studio/activate stores an absolute path, and from then on file operations, static serving, and format dispatch resolve against it. This is what lets one server open projects that live outside its own root .

Format and extension behavior is served from a per-project extension registry , built by scanning the project's dependencies and cached against project.json 's mtime — edit the manifest and the next request rebuilds the registry. The formats , format , and project-schemas routes all answer from it.

The resolve proxies

resolve.ts implements the two endpoints the runtime uses to run server-side code during development. handleResolve takes a $prototype / $src entry, imports the module server-side ( .js directly; .class.json via its $implementation , or a class constructed from the schema), instantiates it with the config, and returns the resolved value. handleServerFunction imports a module and invokes a named export with an arguments object. Both do dynamic import() of project code — they are remote-code-execution surfaces by design, which is why the security model below exists.

code-api.ts adds the code services behind the codeService platform member: oxfmt formatting, oxlint diagnostics, and Bun.Transpiler minification for the function-body editor.

Extension server mounts

jx-mounts.ts dispatches /_jx/* to extension classes that declare a server block — the same fetch-style handlers the generated site worker mounts in production, so data-backed documents work identically in both environments. In development it adds conveniences: env is process.env merged under the project's .dev.vars plus JX_PROJECT_ROOT , connectors with a local provider get stood in locally (D1 becomes SQLite under .jx/data/ ), and table schemas sync additively on first touch. See server mounts for the extension-author side.

The security model

The dev server and the desktop's createProjectServer share one set of primitives ( packages/server/src/net-guard.ts ); the dev server applies all but the token:

  • Loopback bind ( 127.0.0.1 ) by default is the primary control — other local processes and LAN pages can't read a loopback page's location. A hostname option ( jx dev --host ) can widen the bind for containers, but that removes the control and should only be used behind trusted isolation.

  • Origin/Host gate guards every privileged surface — the RCE-capable /__jx_resolve__ / /__jx_server__ routes, the /_jx/ mounts, and the whole /__studio/* API. A loopback (or absent) Origin passes; a cross-origin Origin or a rebinding Host is rejected. The browser Studio and the served site are same-origin, so they pass — a malicious external page does not. The dev server needs no token because it is same-origin; the desktop server, whose canvas iframe is cross-origin, adds a per-server token on top.

  • File containment ( containedPath ) pairs a lexical relative() check with a realpath re-check, so a ../ or absolute path can't escape and a symlink can't point outside the project root. It guards static serving, assertAccessible for the filesystem API, and every $src / $base / $implementation resolved before a dynamic import() . Over-encoded paths are rejected after a single decode.

  • Two-root activation : assertAccessible(filePath, root, activeProjectRoot) allows a path under the server root or the project root Studio bound via POST /__studio/activate . Activation itself accepts four kinds of root: one contained under the server root, an explicit allowedRoots entry, a project this server just created, or a project the account already owns — an absolute directory holding a project.json under the user's home directory. That last kind is what opens an existing project: projects normally live outside whatever tree the server happens to serve. Anything else is a 403 , and a refused activation is an error to show the user — the endpoints that take no dir (the git surface especially) fall back to the server's own root, so swallowing the refusal would quietly act on the wrong tree.

Warning

The resolve proxies import and execute arbitrary project code by design. Keep the loopback bind, and if you must widen it, put the server behind trusted isolation — the Origin/Host gate assumes the network itself is not hostile.