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.
- 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.
- 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).
- 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.
- 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.
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.servethat serves the API (src/server/http.ts, routes/and/api/*). The client is written to that assumption —src/web/api.tsfetches bare paths(
/api/runs) and its own header comment records that requests are same-origin and "CORS isstructurally 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 servefollowed by opening localhost still works with nothing configured.Why auth is the gate, not a polish item
POST /api/runsstarts a coding agent with write access to a real repository andexecon thehost. 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.iocannot callhttp://192.168.1.5:3005— browsersblock 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 anyreverse 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.
management, no self-signed cert generation in
factory serve..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).
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 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.
front of
/api/*; the token on the client side, per server. The gate for everything below.and consistent error shapes; rate limiting. Includes a transport problem worth flagging early:
EventSourcecannot send anAuthorizationheader, so cross-origin authenticated SSE forcesthe 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).src/web/api.ts: a configurablebase 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.
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
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.
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.
someone a run" and "I have to give them the ability to start runs against a real repo."
Not in scope
backend; all state stays on the user's own daemons.