SDK ADR 014: Local Platform Request Proxy
- Status: Accepted
- Date: 2026-09-25
- Owners: Command Center SDK maintainers
- Package:
@dev-mainsequence/command-center-sdk - Contract: none; a new
/viteentry point for the dev server - Amended by: SDK ADR 017: One Machine Session for Local Development
(the token source, the
401handling, and whatplatform_not_configuredcovers; see Amendment) - Related:
Decision summary
A static site deployed in a host sends its platform requests through the host (SDK ADR 013), and
that stays the only path a deployed site takes. A top-level page on a local Vite dev server has no
host, so for local development only the SDK adds platformRequestProxy(), a Vite plugin from the
new /vite entry point. The page sends a platform request to /__mainsequence__/api/... on its own
dev server, which sends it to MAINSEQUENCE_ENDPOINT with the developer's
MAINSEQUENCE_ACCESS_TOKEN, both read from the dev server's environment. The page never holds the
token, and a build never contains it.
Context
With SDK ADR 013 an embedded application holds no platform credential: the host sends each request
as the signed-in person. During local development the page runs top-level, under vite serve, and
nothing hosts it, so every platform request fails and features built on the platform, such as AI
capabilities, cannot be developed locally.
The platform's developer tooling already authenticates a developer with a JWT in the process
environment: the SDK's CLI reads MAINSEQUENCE_ENDPOINT and MAINSEQUENCE_ACCESS_TOKEN. Local
development can use the same variables, provided the token stays in the dev server.
Decision drivers
- The deployed path does not change, and a production build cannot take the local one.
- The page never holds the developer's token; no
VITE_variable, bundle, URL, or browser storage carries it. - No change to the platform, to an application's own API, or to the host.
- A request that works locally has the shape the host bridge carries.
- The route must not let another site, or another computer, act as the developer.
Decision
@dev-mainsequence/command-center-sdk/viteexportsplatformRequestProxy(options?), a Vite plugin that runs only undervite serve.options.pathchanges the route from/__mainsequence__.- The plugin adds one middleware, which runs before Vite serves files. A request under the route is
sent to
MAINSEQUENCE_ENDPOINTplus the rest of the path, withAuthorization: Bearerand the token. The variables are read from the process environment on each request. The plugin makes its own checks rather than rely on Vite's, which depend on Vite's version and configuration. - The request shape is the bridge's:
GET,POST,PUT,PATCH, orDELETE; a path and query under the platform's/api/, resolved before the check;acceptandcontent-type; a body of at most 1 MiB. The status,content-type, and the body come back, withcache-control: no-store. There is no allow-list: the host has one, so a path that works locally can still be refused embedded. - Refusals, each a JSON body with
codeanddetail:not_local(403) when the caller is not this machine or theHostheader is not a loopback name, which stops DNS rebinding;cross_site_request(403) whenSec-Fetch-Siteis other thansame-originornone, orOriginis not the page's;method_not_allowed(405);not_a_platform_api_path(404);request_too_large(413);platform_not_configured(503), naming the missing or invalid variable and never its value;platform_unreachable(502). A401from the platform passes through, and the dev server warns once to refresh the token and restart. - The page selects the local path itself, only when
import.meta.env.DEVis true and it runs top-level, and otherwise callsclient.sendPlatformRequest. The iframe client does not change and never falls back to the dev server. - The published declarations describe only the parts of Vite's dev server the plugin uses, so they
need neither Vite's nor Node's types; a type test proves the plugin fits Vite's
PluginOption.
Compatibility and Mixed Versions
Additive. The /vite entry point is new; no existing export, contract, message, or behavior
changes. Applications that do not add the plugin are unaffected. The plugin works with any Vite
dev server whose middleware stack is connect-compatible, which the type test pins.
Host Handoff and Backend Impact
None. The platform receives the developer's requests with the developer's own token, exactly as it receives the SDK CLI's. Hosts are not involved in local development.
Alternatives Considered
- A token in a
VITE_variable, read by the page. Rejected: Vite writes those into the bundle, so a build made on that machine would ship the token, and the page would hold a credential. - A sign-in inside the site for local development. Rejected: every application would carry development-only authentication code, and the developer's password would go into a development page.
- Forwarding through the application's own API. Rejected: a static site may have no API, and the API would carry development-only code.
- Running a host locally. Rejected: developers of static sites do not run the host.
- A fallback inside
createStaticSiteIframeClientwhen there is no parent. Rejected: the client would guess between transports; the page chooses explicitly, and the client keeps one path. - Plain Vite
server.proxyconfiguration with anAuthorizationheader. Rejected: it adds the token to every request that reaches it. Vite's own host and CORS checks depend on its version and configuration; an HTTPS dev server has no host check, and the default CORS lets any localhost page read responses.
Consequences
- AI capabilities and other platform features can be developed on a top-level local page with the same request shape as the deployed site.
- Local development has no allow-list, so embedded testing before release stays necessary.
- The token lives in the dev server's environment until the developer replaces it; an expired
token is a
401until the dev server restarts with a new one.
Amendment 2026-09-30: the saved session
SDK ADR 017 changes where the token comes from. The text above stays as the record of the original decision; where the two differ, this section and SDK ADR 017 hold.
- Decision 2:
MAINSEQUENCE_ACCESS_TOKENis used when the dev server's environment sets it, and goes only to theMAINSEQUENCE_ENDPOINTset next to it. Without it, the token is the sessioncommand-center-sdk loginsaved on this machine for the backend the project names: inMAINSEQUENCE_ENDPOINTof the environment, in the project's.env, in the settings the CLIs share, or the standard platform. The plugin keeps the access token in memory until it is about to expire and renews it by itself. - Decision 4: a
401to a session token is answered by renewing the session and sending the request once more; a second401passes through with one warning that names the login command. A401to an environment token passes through as before.platform_not_configuredalso covers no usable session, no credential store, and a store that could not be read. - Consequences: with a saved session, the dev server keeps working while the session lasts, and nobody restarts it for a token.