The dev server
During development you don't run the compiled
dist/
output — you run your source files through the Jx dev server, a Bun-native server from
@jxsuite/server
. It serves the project directory as-is, reloads the browser when files change, executes server-side code on your behalf, and backs Studio's file operations.
Starting it
The whole server is one call,
createDevServer
:
// server.js
import { createDevServer } from "@jxsuite/server";
await createDevServer({
root: import.meta.dir,
port: 3000,
});
Run it with
bun run server.js
and open
http://localhost:3000/
. The full options surface:
| Option | Default | What it does |
|---|---|---|
root |
(required) | Project root to serve; every file operation is contained to it. |
port |
3000 |
Listen port. |
builds |
[] |
Bun.build
entries (
entrypoints
,
outdir
, optional
match
,
label
) bundled at startup and selectively rebuilt when a changed file matches. |
watch |
true |
File watching + live reload. Pass
false
to disable, or an options object for the watcher. |
studio |
true |
Mounts the
/__studio/*
API that Jx Studio talks to. |
middleware |
— | (req, url) => Response | null
— your own routes, checked before static file serving. |
For a site project,
jx dev
is the front door: it runs this server under Bun with a site-aware wrapper that builds the project up front, serves the built pages from
dist/
, and rebuilds before each live-reload broadcast. A hand-written
server.js
like the one above is for embedding the server with custom options.
Live reload
The server watches
root
(ignoring
node_modules/
,
dist/
,
.git/
, and friends) and exposes a Server-Sent Events endpoint at
/__reload
. Every
.html
file it serves gets a one-line client injected before
</body>
:
<script>
new EventSource("/__reload").onmessage = () => location.reload();
</script>
Save a file and every connected page reloads. When the changed file matches a
builds
entry's
match
pattern, that bundle is rebuilt first, so the reload picks up fresh output. The one exception is the Studio editor itself: Studio pages never get the reload script, because Studio refreshes edited files in place and a full reload would discard open tabs and undo history.
How Studio is served
Studio is a static web app plus a REST API — the dev server provides both. With
studio: true
(the default), the server mounts
/__studio/*
: project metadata, file listing, read/write/delete/rename, component discovery, content search, code formatting and linting for the function-body editor, and a realtime co-editing WebSocket at
/__studio/collab
. Every filesystem operation is validated to stay under
root
, so path traversal is rejected.
Studio's UI assets are ordinary static files under the served root; opening a project in Studio activates its directory on the server, which then also resolves project files, and
public/
contents at the site root, exactly as the production build would. The desktop app doesn't use this server — it embeds its own loopback-only, token-gated variant — but it speaks the same API.
Running server-side code: the two proxies
Production builds compile server-side work into generated handlers. In development there is no build, so the runtime hands that work to the dev server through two POST endpoints. You don't call either one yourself — they exist so documents behave the same in dev as after
jx build
.
Module resolution (
POST /__jx_resolve__
)
When a document uses an external class — a
$prototype
entry with a
$src
— the browser can't always resolve it: the module may need Node-only APIs like the filesystem, or live behind CORS. The runtime posts the entry (its
$src
,
$prototype
,
$export
, and config) to the dev server, which imports the module server-side (
.js
directly,
.class.json
via its
$implementation
), instantiates the class with the config, resolves it, and returns the value as JSON. Reactive entries re-resolve when their inputs change.
Server functions (
POST /__jx_server__
)
Functions marked
timing: "server"
never ship to the browser. In development the runtime posts the call instead:
{
"$src": "./dashboard.server.js",
"$export": "fetchMetrics",
"arguments": { "userId": 42 }
}
The server imports the module, invokes the export with the arguments object, and returns the result as JSON. In production the same calls hit the generated server handler — see How compilation works .
Extensions that declare server mounts (for example the data API) are served under
/_jx/*
with the same wire contract as the generated production worker, so data-backed documents work identically in both environments.
Static files and npm packages
Anything the other routes don't claim is served from disk: files under
root
at their natural URLs, then files under the active Studio project, then the project's
public/
directory mapped to the site root — mirroring where assets live in production. Bare npm specifiers in URLs (such as
@jxsuite/parser/…
) are resolved through
node_modules
, bundled on demand with
Bun.build
, and cached for the life of the server. All responses are sent with
Cache-Control: no-cache
so a plain reload never serves a stale bundle.
Related
CLI commands — what
bun create @jxsuitescaffolds and whatjxcan runHow compilation works — what replaces the proxies in production
Site architecture — the directory layout the server serves