The backend protocol
The Studio Backend Protocol is the wire contract every Jx Studio backend implements. It lives in one package,
@jxsuite/protocol
, with two entrypoints.
@jxsuite/protocol/types
holds the request/response shapes (
DirEntry
,
GitStatusResult
,
StarterInfo
, and friends), which are environment-agnostic and importable in browsers, Bun, and Workers alike.
@jxsuite/protocol/routes
holds
STUDIO_ROUTES
, the canonical endpoint table. If you're serving the protocol over HTTP, this page plus the
route reference
is your specification; if you're
writing a platform adapter
instead, these are still the semantics your adapter must preserve.
What exactly is the contract
The
sub-path, method, and body shapes are the contract
. The transport and path prefix are not. The dev server serves the literal
/__studio/*
routes; the desktop app dispatches the same operations over typed RPC / WebSocket JSON-RPC; cloud platforms serve them over HTTP behind a session gateway prefix. All three answer the same shapes for the same sub-paths, so Studio can't tell them apart.
Each entry in
STUDIO_ROUTES
carries its method, path, a one-line contract summary, and, for optional routes, what Studio does without it:
// packages/protocol/src/routes.ts
export interface StudioRoute {
path: string;
method: StudioRouteMethod;
/** True when a backend may omit the route (its PAL member is optional). */
optional: boolean;
/** One-line contract summary. */
summary: string;
/** What Studio does when an optional route is absent. */
degradation?: string;
}
The full table is generated into the protocol route reference . Consult it there rather than re-reading the source.
Core vs optional
Optional routes back optional
StudioPlatform
members. Omitting one is not an error. Studio degrades exactly as the entry's
degradation
describes: the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing instead, and so on.
coreRouteNames()
and
optionalRouteNames()
split the table programmatically, so a conformance test for a new backend can iterate
STUDIO_ROUTES
and assert that every core route answers.
The core set is what a minimal backend must serve: project session and probing (
activate
,
project
,
project-info
,
resolve-site
,
create-project
), the filesystem CRUD family, component discovery, package listing, the git suite, and the AI proxy pair (
ai/chat
,
ai/models
). Everything else is optional: collab, starters, the data surface, secrets, Cloudflare publishing.
Versioning
STUDIO_PROTOCOL_VERSION
(currently
1
) bumps whenever any route's request or response shape changes incompatibly. The types are published as TypeScript source on the monorepo's release train; the package's only runtime dependency is
@jxsuite/schema
.
The error shape
Every failure is an
RFC 9457
problem document, sent as
application/problem+json
:
{
"type": "https://jxsuite.com/problems/conflict",
"title": "The request conflicts with the current state",
"status": 409,
"detail": "Pull stopped: 2 file(s) changed on both sides.",
"conflicts": ["src/app.json", "README.md"]
}
Four things are worth knowing before you implement one:
typeis what Studio keys on , and it's an absolute URI. The full list is in Studio routes , generated from the same table the reference backend answers from, so it can't drift.titledescribes the type ;detaildescribes this failure. Keeptitleidentical across occurrences and put the specifics indetail. Swapping them is the standard's most common misuse, and it's what stops Studio grouping two instances of the same problem.The status belongs to the type. If you find yourself wanting to send one type at two statuses, it's two types.
Extension members carry anything else the type documents :
conflictson a pull conflict,installUrlon a missing GitHub App installation. Studio's recovery UI reads them.
error
is emitted alongside
detail
, with the same value, for one release. It's the pre-RFC-9457 field name, kept so a client written against the older shape keeps working while it migrates. Don't write new readers against it.
Three surfaces deliberately answer 200 with an error field instead , and a conforming backend should too:
| Surface | Why |
|---|---|
Code services (
/code/*
) |
A syntax error in the author's snippet is the result of a lint, not a failed request |
| AI model catalogue | Degraded success: the catalogue is still delivered, from defaults |
| Mid-stream SSE frames | The response began with a 200; nothing can change the status, so the frame carries a problem |
Behaviors implementers must match
Route shapes alone don't capture everything. These semantics are part of the contract:
git/commitcommits the staged files if any are staged, otherwise all dirty files. Cloud backends may make commit+push atomic and treatgit/pushas a sync check (aheadstays 0).git/pullreturns409 { code: "pull_conflict", conflicts: [paths] }when local dirty files overlap remote changes; a clean pull fast-forwards.formatdispatches{ format, action: "parse" | "serialize", source? | doc?, options? }through the project's format registry. Without it, only.jsondocuments open.ai/chataccepts{ messages, tools, systemPrompt, model }and streams the normalizedStreamEventSSE defined by@jxsuite/ai/streaming-client. The union and its six members are exported, so implementers can import the type they are satisfying rather than reconstruct it.ai/modelsreturnsAiModelsResponse; reportconfigured: truewhen the backend holds credentials (Studio then unlocks the assistant without a locally stored key) andmanaged: truewhen the platform brokers them. It is a capability probe, so it answers200in every credential state. A non-2xx makes the probe throw, which withdraws the connect affordance from exactly the users who need it. Where a model's capabilities are known, report them per entry:toolSupport: falsemakes Studio warn that the chosen model will answer but never edit, andcontextWindowreplaces Studio's model-id prefix table, which recognizes only the families it was written for and budgets everything else at its default. A brokered backend distinguishescf_not_connected(invite),cf_reconnect_required(the grant lapsed, so explain and offer reconnection),cf_account_required(the grant is live but no account is chosen; offer a picker, since re-authorizing lands back here) andcf_upstream_error, which is reported withconfigured: truebecause a briefly unreachable provider is not a credential problem.collabis a WebSocket upgrade speaking the@jxsuite/collabwire envelope: one socket per project, documents multiplexed by path. A plain GET (no Upgrade) answers{ collab: true, protocols, version }as the capability probe.protocolslists the WebSocket subprotocols the backend speaks, currently["jx.collab.v1"], and it is also the negotiation: Studio offers one of them asSec-WebSocket-Protocoland the backend must echo the one it accepts. A backend that lists none is offered none , because RFC 6455 §4.1 makes a client whose offer went unechoed fail the connection. An older backend keeps working exactly as before. A backend that lists only tokens Studio cannot parse gets no socket at all, which is the point: two peers whose envelopes disagree would merge divergent histories.filesanswers in stable path order , both when listing a directory (?dir=) and when searching (?glob=).readdirand glob scans report in filesystem order, which varies with a directory's write history; Studio's collection grid inserts rows in listing order, so an unsorted listing reaches the user as a table that reshuffles itself between opens. Sort by codepoint rather than locale collation, so two backends and two machines agree.cf/proxyis an allowlisted Cloudflare API passthrough (accounts and Pages projects/deployments only). The backend injects credentials (a header-borne user token on the dev server, a vaulted OAuth token on cloud), and stateless implementations must never persist them.The data routes (
data/connections,data/rows,data/push, …) intentionally bypass table permission rules. They are the owner console, and the backend boundary (loopback/token locally, collaboration permission on cloud) is the gate. The secrets routes carry env-var names only , never values. See connectors and the extension security model .
Related
Protocol route reference : the generated table of every route, method, summary, and degradation
Dev server internals : the reference implementation of these routes
Writing a platform adapter : the in-page interface these routes back