Skip to content

Tracker: the hosted control plane — factory's UI as a static app over many servers #31

Description

@FreshlyBrewedCode

Where we are today

A factory daemon is a self-contained appliance. One process owns one project, and it serves its
own SPA off the same Bun.serve that serves the API (src/server/http.ts, routes / and
/api/*). The client is written to that assumption — src/web/api.ts fetches bare paths
(/api/runs) and its own header comment records that requests are same-origin and "CORS is
structurally impossible."

There is no authentication of any kind. Reachability is authorization.

Where we want to go

The UI stops being an artifact of the daemon and becomes a control plane: a static web app,
deployed once to a public URL (GitHub Pages), that holds your list of factory servers, connects
to each one from the browser, and presents them as a single surface. The daemon becomes a headless,
authenticated, cross-origin API. "Which project am I looking at" stops being a property of the
process you happened to open and becomes something you pick in the client.

The zero-config local path does not go away: a daemon keeps serving its own bundled UI, so
factory serve followed by opening localhost still works with nothing configured.

Why auth is the gate, not a polish item

POST /api/runs starts a coding agent with write access to a real repository and exec on the
host. The API is a remote-code-execution surface by design. Everything else in this tracker is
blocked behind authentication, because every other step in it increases the number of places the
API can be reached from.

The hard constraint: reachability, not auth

A page served from https://<user>.github.io cannot call http://192.168.1.5:3005 — browsers
block mixed content. This is the single fact that decides whether the public-hosted model works,
and it is a networking problem that no amount of auth design solves.

Direction: factory stays out of the TLS business. The daemon keeps listening over plain HTTP,
and exposing it over HTTPS is the operator's job — tailscale serve, cloudflared, Caddy, or any
reverse proxy. We document the pattern and make sure nothing in the API or client fights it
(host/proto headers, CORS, no absolute-URL generation server-side).

Decisions

Recorded here at a high level. Anything that turns out to need a dedicated ADR gets one as part of
the ticket that implements it.

  • Reachability is the operator's problem. Tunnel or reverse proxy, per above. No cert
    management, no self-signed cert generation in factory serve.
  • One project per server, for now. A daemon keeps owning exactly one .factory/ folder
    (ADR 0008). Multi-project is a client-side concern: the registry holds N server URLs and the UI
    aggregates. This leaves config, admission, concurrency and persistence untouched. Servers hosting
    multiple projects stays on the table as a later direction — so the API should be shaped not to
    foreclose it (project-scoped paths cost nothing to reserve now).
  • A shared bearer token per server. Issued and rotated by the CLI, stored per registered server
    in the client. No identity provider; this is a single-operator tool. Read-only scopes and real
    user identity (OIDC) are deferred, not rejected.
  • The daemon keeps serving its bundled UI. The static deploy is an additional distribution of
    the same SPA, not a replacement. Losing factory serve → open localhost would be a regression.

Workstreams

Rough dependency order. Each of these is an epic to be broken down when it comes up — none are
filed yet.

  1. Authentication. Token issuance, storage and rotation in the CLI; a verifying middleware in
    front of /api/*; the token on the client side, per server. The gate for everything below.
  2. API hardening. CORS with an explicit allowlist; a versioned, stable surface; validated edges
    and consistent error shapes; rate limiting. Includes a transport problem worth flagging early:
    EventSource cannot send an Authorization header, so cross-origin authenticated SSE forces
    the token into the query string or makes the SSE client fetch-based — a real change to the
    single transport seam ADR 0011 deliberately left in one file (subscribeToRun).
  3. Client independence. Kill the same-origin assumption in src/web/api.ts: a configurable
    base URL, a locally-persisted server registry, per-server connection health and error isolation
    (one unreachable server must not break the page), and aggregate views across servers. Plus a
    standalone static build target, since the SPA is currently only ever built by the daemon.
  4. Hosting and compatibility. Deploy the static bundle to Pages from CI. Then the problem that
    only exists once it is hosted: an evergreen client will outrun pinned daemon versions, so the
    client needs to detect and degrade against an older API rather than break.

Open questions

  • Does the aggregate multi-server view mean "a server switcher" or "one merged runs list across
    servers"? The second is much more interesting and much more expensive — it needs stable
    cross-server run identity and a merge/sort story for lists that today come from one SQLite file.
  • Where does the server registry live? Browser local storage is the obvious start and means the
    hosted app holds no state at all, but it also means your server list does not follow you between
    devices and is lost by a cache clear.
  • Do we want a read-only token scope early after all? It is the difference between "I can show
    someone a run" and "I have to give them the ability to start runs against a real repo."

Not in scope

  • Factory hosting anything on a user's behalf. The hosted artifact is a static bundle with no
    backend; all state stays on the user's own daemons.
  • Multi-user collaboration, per-user audit trails, or anything else implied by real identity.
  • Multi-project servers (see Decisions).
  • Replacing the daemon-served UI.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    trackerHigh-level hub issue: vision and direction, links out to epics and tasks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions