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:
/__reload— the SSE live-reload endpoint (skipped whenwatch: false).POST /__jx_resolve__— the$prototype/$srcresolution proxy.POST /__jx_server__— thetiming: "server"function proxy./_jx/*— extension server mounts./__studio/*— the Studio API (skipped whenstudio: false): the collab WebSocket upgrade,activate, then the AI, site-import, and code sub-APIs, thenhandleStudioApifor everything else.Custom
middleware, if the embedder passed one.Static files: project files at their natural URLs, the active project's tree,
public/at the site root, and bare npm specifiers resolved throughnode_modulesand 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. Ahostnameoption (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 lexicalrelative()check with arealpathre-check, so a../or absolute path can't escape and a symlink can't point outside the project root. It guards static serving,assertAccessiblefor the filesystem API, and every$src/$base/$implementationresolved before a dynamicimport(). 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 viaPOST /__studio/activate. Activation itself accepts four kinds of root: one contained under the server root, an explicitallowedRootsentry, a project this server just created, or a project the account already owns — an absolute directory holding aproject.jsonunder 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 a403, and a refused activation is an error to show the user — the endpoints that take nodir(the git surface especially) fall back to the server's own root, so swallowing the refusal would quietly act on the wrong tree.
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.
Related
The dev server — the user-level page: options, live reload, the proxies from the document author's view
The backend protocol — the contract this server is the reference for
Protocol route reference — every
/__studio/*route it servesExtension security model — the trust boundaries extensions themselves live under