Smithers · developer + infrastructure map

How our apps reach their backends,
why local sign-in is dead,
and the plan to fix it.

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.

apps/smithers · web (Cloudflare) apps/smithers · Electrobun native (planned) Plue / Gateway / Cerebras / GitHub App
June 2026 · ← / → or Space to move · press F for fullscreen
the stack

One app, two targets. Two rehearsals.

🌐 apps/smithers web · shipping

Chat-first PWA, Vite + React + Zustand. Deploys to Cloudflare as a single Worker via Alchemy. This is "the app".

🖥️ apps/smithers Electrobun native · planned

The same codebase, packaged as a desktop app. Already Electrobun-aware (src/app/history.ts), but no packaging exists yet.

🧪 two POCs retired

~/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).

the services behind the apps

Six services. The app owns none of them.

Plue / jjhub

Go monolith. Identity + OAuth (/api/user, /api/auth/*) and code hosting (repos, issues, landings). Local: docker-compose. Prod: GKE.

Smithers Gateway

packages/server, a Hono server on 127.0.0.1:7331. Owns runs: /v1/rpc, /workflows, /health. SQLite-backed.

Cerebras chat

Runs inside the Cloudflare Worker at /api/chat (gpt-oss-120b). The API key is a Worker secret and never reaches the browser.

GitHub App

External OAuth provider behind the "Continue with GitHub" button. Plue does the redirect dance and exchanges the callback for the user session.

Electric (ElectricSQL)

Real-time Postgres sync. Lives at electric.smithers.sh in prod (its own Helm chart + auth proxy). Not on the sign-in path.

Freestyle

Workspace sandbox VMs + SSH (vm-ssh, ssh-gateway). Where agent runs execute. External managed service.

the one idea that explains everything

The web app only ever calls same-origin /api/*.

apps/smithers
browser · fetch("/api/user")
the proxy
vite (dev) · CF Worker (prod)
Plue · auth + platform
Gateway · runs
Cerebras · chat

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.

apps/smithers/src/auth/authClient.ts · authFetch() → fetch("/api/...") · apps/smithers/vite.config.ts · apps/smithers/src/worker.ts
the proxy is the whole game

One table maps every route to a backend.

/api/auth/*
/api/user
Plue identity + OAuthAUTH_API_BASE_URL · SMITHERS_AUTH_PROXY_TARGET
/api/repos, /api/orgs
/api/user/<sub>
Plue / jjhub code hostingGO_API_BASE_URL · SMITHERS_PLATFORM_PROXY_TARGET
/v1/rpc, /workflows
/health
Smithers Gateway (runs)GATEWAY_BASE_URL · SMITHERS_GATEWAY_PROXY_TARGET
/api/chat
Cerebras (server-side in the Worker)CEREBRAS_API_KEY · Worker secret

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.

the actual bug

Bare 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:

# token sign-in validates against /api/user GET /api/user → 200 OK content-type: text/html # the SPA, not a user # "Continue with GitHub" navigates here GET /api/auth/github/authorize → 200 OK content-type: text/html # reloads the app
  • Token sign-in: stores the token, calls /api/user, gets HTML, response.json() throws, silently flips to signed-out.
  • OAuth buttons: window.location = "/api/auth/..." just re-serves the app. No provider is ever reached.
  • The auth code is fine. It is a missing-backend problem, not a logic bug. Wire a proxy and the same code works.

Proxy vars that were unset: SMITHERS_AUTH_PROXY_TARGET, SMITHERS_GATEWAY_PROXY_TARGET, SMITHERS_CHAT_PROXY_TARGET.

where Plue actually lives

The live backend is api.jjhub.tech. The default points at a ghost.

✓ api.jjhub.tech — live

GET /api/user → 401 {"message":"authentication required"} GET /api/auth/github/authorize → 302 github.com/login/oauth/authorize client_id=Iv1...

Real Plue, on GKE + Cloud SQL. Serves exactly the routes the app needs.

✗ api.smithers.sh — dead

GET / (any path) → 404 DEPLOYMENT_NOT_FOUND (Vercel)

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.

getting a backend in dev

Three ways to feed the proxy.

OptionWhat it isCostSign-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.

option B, in detail

Plue's local stack is one compose file.

postgres
:5432
migrate
atlas
seed
alice / bob
api
:4000 · /api/*

repo-host :8080

Git storage + push-hook callbacks.

ssh :2222

Git over SSH (jj push / pull).

fake-gcs :4443

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).

/Users/williamcory/plue/docker-compose.yml · GitHub App creds default empty · SMITHERS_ENABLE_E2E_TEST_ROUTES=true
the honest caveat

The token box works locally. The buttons need the Worker.

Token sign-in — works through plain vite

The proxy forwards your Authorization: Bearer header straight to Plue, which validates it with no cookies involved. Paste a token, you are in.

OAuth buttons — need same-origin help

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.

  • The Cloudflare Worker fixes this in prod: it rewrites the GitHub 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.
  • Real OAuth also needs GitHub App credentials in Plue's env (SMITHERS_AUTH_GITHUB_CLIENT_ID/SECRET), which the local compose leaves empty by default.
  • Local Plue with test routes on does dev-auto-authorize as user 1 (alice/admin), but only on the first-party /api/oauth2/authorize PKCE flow (CLI / native), not the web GitHub button.
the electrobun target

Same code, no server. The shell does the fan-out.

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.

  • The Electrobun main process (Bun) must fan out /api/* to Plue and the gateway itself, the role the vite/Worker proxy plays on the web.
  • Prior art exists: the studio-2 POC did exactly this, an RPC bridge + workspace API to the gateway on :7331. Adapt it, do not reinvent.
  • Routing and auth logic stay shared. Only the transport seam changes per target.

Distribution — not built yet

The web target deploys via Alchemy. The Electrobun target needs its own pipeline: build, sign + notarize, distribute and update.

# reference pattern (from the ~/gui POC) electrobun build → sign + notarize → upload to Cloudflare R2download.smithers.sh

~/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.

production topology · what is codified

Three IaC stacks today. One infra owner tomorrow.

🌐 Web · apps/smithers

Alchemy → Cloudflare Worker. Serves the SPA, runs /api/chat, proxies auth/platform/gateway. Secret: CEREBRAS_API_KEY.

codified apps/smithers/alchemy.run.ts

⚙️ Backend · Plue

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}

🖥️ Native · Electrobun

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 decision · deploy ownership

Plue owns all infra and vendors the app.

Deploy path

smithers repo
pinned SHA
↓  git submodule
Plue CI
pnpm install → build apps/smithers
Cloudflare Worker
Plue secrets + DNS

The whole repo is vendored (apps/smithers needs packages/* via workspace:*). Bumping the pin is the release.

Who owns what

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)

Scope & the one open call

  • Web app only. Electrobun native deferred to a later decision.
  • Worker deploy: Plue invokes the app's Alchemy with injected secrets (recommended; proxy + chat logic stays with the app), or re-expresses it as Plue terraform.
Consumption: git submodule / source · accepted coupling: a smithers build break = a Plue deploy break (mitigate with an auto pin-bump PR in Plue)
the dev bridge · fixes sign-in

One command brings the backend up and wires it in.

A dev:full orchestrator

  • Read PLUE_DIR (assume Plue is cloned; default to the known path, fail clearly if absent). Convention already in dev-with-plue.sh.
  • Boot Plue's compose from $PLUE_DIR, wait for :4000.
  • Boot the local gateway (smithers up, :7331).
  • Start the app with every proxy wired (auth → :4000, gateway → :7331, chat). Sign-in works on first run.

Still open (dev only)

  • Default dev mode: plain vite + token sign-in (simple), or the Worker via alchemy dev for full OAuth (faithful to prod)?
  • GitHub App dev credentials wired so the buttons work locally, or is the token path enough for now?

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.

Docs-first per repo convention: a spec lands before code.
1 / 14
← → · Space · F