One app, two shipping targets (web and Electrobun native), one backend family. This deck traces every service the app needs, shows the exact reason sign-in does nothing on a fresh checkout, and lays out a dev setup that mirrors Plue and stays infra-as-code.
Chat-first PWA, Vite + React + Zustand. Deploys to Cloudflare as a single Worker via Alchemy. This is "the app".
The same codebase, packaged as a desktop app. Already Electrobun-aware
(src/app/history.ts), but no packaging exists yet.
~/gui (Swift) and apps/smithers-studio-2 (Electrobun/React) were
built to inform apps/smithers, then set aside. Not in the stack. Ignore unless asked.
Most of this deck is the web target and the services both targets share. The Electrobun target runs the same code and reaches the same backends a different way (slide 11).
Go monolith. Identity + OAuth (/api/user, /api/auth/*) and
code hosting (repos, issues, landings). Local: docker-compose. Prod: GKE.
packages/server, a Hono server on 127.0.0.1:7331.
Owns runs: /v1/rpc, /workflows, /health. SQLite-backed.
Runs inside the Cloudflare Worker at /api/chat
(gpt-oss-120b). The API key is a Worker secret and never reaches the browser.
External OAuth provider behind the "Continue with GitHub" button. Plue does the redirect dance and exchanges the callback for the user session.
Real-time Postgres sync. Lives at electric.smithers.sh in prod (its own Helm chart + auth proxy). Not on the sign-in path.
Workspace sandbox VMs + SSH (vm-ssh, ssh-gateway). Where agent runs execute. External managed service.
The app has no backend URLs baked in and no CORS to negotiate.
It fetches relative paths. Something in the middle must fan them out.
In dev that is the vite proxy. In prod it is the Cloudflare Worker. If nothing is wired,
every /api/* call hits the static site instead. That is the whole bug.
Left column: what the browser asks for. Right column: where it goes, and the
env var that wires it (prod binding · dev proxy var). The Worker also re-authenticates the
gateway hop: it strips the browser token and injects trusted-proxy headers
(x-user-id, x-user-scopes) after validating the Plue session.
pnpm dev wires none of it.The dev proxies only turn on when their env var is set. A plain checkout sets
nothing, so /api/user and /api/auth/* fall through to the SPA fallback
and return index.html. Measured against the running dev server:
/api/user,
gets HTML, response.json() throws, silently flips to signed-out.window.location = "/api/auth/..."
just re-serves the app. No provider is ever reached.Proxy vars that were unset:
SMITHERS_AUTH_PROXY_TARGET, SMITHERS_GATEWAY_PROXY_TARGET,
SMITHERS_CHAT_PROXY_TARGET.
Real Plue, on GKE + Cloud SQL. Serves exactly the routes the app needs.
The canonical default in code and the CLI. DNS resolves to a torn-down
Vercel project. smithers.sh itself is a Next.js marketing site and does not
proxy /api/*.
Takeaway: point dev at api.jjhub.tech or a local Plue, never the smithers.sh default.
| Option | What it is | Cost | Sign-in that works |
|---|---|---|---|
| A · fake Plue | tests/fixtures/fakePlueHost.ts — in-repo deterministic auth host.
No Docker, no network. |
seconds | Token box, seed tokens smithers_e2e_user_token |
| B · local Plue | scripts/dev-with-plue.sh boots Plue's docker-compose and points the proxies
at it. Real Go API on :4000. |
Docker + a Plue clone | Token box (real users); OAuth needs GitHub App creds |
| C · remote Plue | SMITHERS_AUTH_PROXY_TARGET=https://api.jjhub.tech pnpm dev
. Real prod identity. |
one env var | Token box only (OAuth cookie-jar, slide 10) |
All three already exist in the repo or as one-liners. The user's
ask is to make Option B a first-class, repeatable thing driven from the smithers
repo, with a PLUE_DIR-style pointer to the Plue clone.
Git storage + push-hook callbacks.
Git over SSH (jj push / pull).
Mock object storage. Optional fake-sandbox profile for Freestyle.
The api serves /api/user and
/api/auth/* on port 4000 — exactly what the web app's auth proxy needs.
Boot order is enforced by depends_on. Plue's own command is
docker compose up -d --build (wrapped by zig build docker-up).
The proxy forwards your Authorization: Bearer header straight to Plue, which
validates it with no cookies involved. Paste a token, you are in.
GitHub sends the browser back to the auth origin's /callback, so the session
cookie lands on a different origin than localhost. Different cookie jar, still 401.
redirect_uri to its own origin (AUTH_CALLBACK_BASE_URL), so the callback and
the app share one cookie jar. Running the Worker locally (alchemy dev) reproduces that.SMITHERS_AUTH_GITHUB_CLIENT_ID/SECRET), which the local compose leaves empty by default./api/oauth2/authorize PKCE flow (CLI / native), not the web GitHub button.The native app is apps/smithers wrapped in Electrobun, not a
separate codebase. The webview loads the built bundle over a custom scheme
(views://) with no HTTP server behind it. So the web app's trick, same-origin
/api/* proxied by vite or the Worker, has nothing to proxy it.
/api/* to
Plue and the gateway itself, the role the vite/Worker proxy plays on the web.The web target deploys via Alchemy. The Electrobun target needs its own pipeline: build, sign + notarize, distribute and update.
~/gui already proved the notarize → R2 → download flow for a Swift DMG. The Electrobun app can mirror it. None of it is wired in apps/smithers today.
Alchemy → Cloudflare Worker. Serves the SPA, runs /api/chat,
proxies auth/platform/gateway. Secret: CEREBRAS_API_KEY.
codified apps/smithers/alchemy.run.ts
Terraform + Helm → GKE. Cloud SQL Postgres 16, Electric sync, ssh-gateway, preview-gateway. Served at api.jjhub.tech. Deploy on git tag.
codified plue/infra/{terraform,helm}
Package apps/smithers as a desktop app. Build, sign + notarize, distribute. Mirror the ~/gui POC's R2 + download.smithers.sh flow.
deferred later decision; no packaging yet
Today these are three separate stacks: Alchemy here for the web, Terraform + Helm in Plue for the backend, nothing yet for native. The decision: fold the web deploy into Plue so there is one infra owner. Next: the deploy inversion, then the dev bridge that fixes sign-in.
The whole repo is vendored (apps/smithers needs
packages/* via workspace:*). Bumping the pin is the release.
| Plue | DNS · secrets · environments · the version pin · the deploy trigger |
| smithers | app source · the worker bundle · tests · the dev loop (pnpm dev → Plue compose, unchanged) |
dev:full orchestratorPLUE_DIR (assume Plue is cloned; default to the known path, fail clearly if absent). Convention already in dev-with-plue.sh.$PLUE_DIR, wait for :4000.smithers up, :7331).alchemy dev for full OAuth (faithful to prod)?Dev shells out to $PLUE_DIR, the same Plue
clone the deploy path vendors. The dev loop stays in the smithers repo, untouched by the deploy inversion.