← Corpus / augment-it / issue
Every remote hardcodes the workspace WebSocket to localhost — Org Workbench loads no data on augment.didi.sh
Sixteen remotes dial `ws://localhost:3001/ws` with no env read. On prod that points at the visitor's own laptop, so the socket shows `closed` and the roster never fills.
- Path
- issues/Every-Remote-Hardcodes-The-Workspace-WS-To-Localhost-So-Prod-Loads-No-Data.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Opus 5
- Tags
- Issue · Augment-It · Org-Workbench · Deployment · Microfrontends · WebSocket · Reach-Edu · Module-Federation
Every remote hardcodes the workspace WS to localhost
Why Care?
On https://augment.didi.sh, the Org Workbench surface for the
reach-edu workspace renders its chrome — title, SurrealDB · Organizations
badge, client: reach-edu, search box, + New organization — and then shows
nothing. No coverage roster, no organizations, no people. A red closed
pill sits in the top-right corner. The same surface, same workspace, same commit
works perfectly on localhost:3100.
That “works local, dead on prod” split is the tell, and it is not a data problem, a SurrealDB problem, or a tenancy problem. The remote is asking the visitor’s own laptop for its data.
The root cause — one line, no env read
apps/org-workbench/src/App.svelte:20:
const WS_URL = 'ws://localhost:3001/ws';
That is the whole bug. Compare the pattern the shell, chat, and
corpora-curator all use correctly — for example shell/src/App.svelte:41-43:
const WS_URL =
((import.meta as { env?: Record<string, string> }).env?.PUBLIC_WS_URL as string | undefined) ||
'ws://localhost:3001/ws';
rsbuild inlines PUBLIC_-prefixed vars into import.meta.env at build time.
Org Workbench never performs that read, so no build-time value can reach it.
The deployment looks correctly configured, which is what makes this
expensive to spot. apps/org-workbench/Dockerfile:23-28 faithfully declares
and exports the variable:
ARG PUBLIC_WS_URL
ENV PUBLIC_WS_URL=$PUBLIC_WS_URL
and Railway sets it per [[../../DEPLOYMENT]]. Every rung of the config chain is green. The value simply lands in a build environment that no source line ever consults, and is dropped on the floor.
Two independent reasons it can never work in production
- Wrong host.
localhost:3001in a browser onaugment.didi.shresolves to the viewer’s machine, not Railway’sworkspace-service. It works on the operator’s laptop for the accidental reason that the laptop really is runningworkspace-serviceon:3001— the local stack masks the defect perfectly. - Mixed content. Even if a viewer did run the backend locally, an
insecure
ws://connection is blocked outright by every modern browser when the page origin ishttps://. Prod needswss://ws.augment.didi.sh/ws.
Blast radius — this is a family defect, not one surface
Sixteen remotes carry the identical hardcoded constant:
| App | Line | Deployed to prod? |
|---|---|---|
org-workbench | App.svelte:20 | yes — broken, this report |
search-and-add | App.svelte:22 | yes — same break |
search-results | App.svelte:16 | yes — same break |
corpora-curator | App.svelte:9 | yes — works anyway, see below |
chat | App.svelte:20 | yes — correct, reads env with localhost fallback |
record-collector, records-surface, pack-runner, sort-filter-lens, person-db-resolver, record-db-resolver, affiliation-rating-resolver, enhanced-records-list, prompt-template-manager, request-reviewer, response-reviewer | various | no — latent, will break on the day they deploy |
The entire Augment-from-DB flow is down on prod, not just Org Workbench —
org-workbench, search-and-add, and search-results are the three services
that flow comprises, and all three share the bug.
Two nuances worth recording:
corpora-curatorworks by luck of file layout. ItsApp.svelte:9has the same dead hardcoded constant, but its real client lives insrc/curation.svelte.ts:21-23, which does readPUBLIC_WS_URL. The unused constant inApp.svelteis a live trap for the next person who wires a socket there.chatis the reference implementation.apps/chat/src/App.svelte:16-20gets it exactly right, comment included.
This is a different axis from [[Move-Remaining-Remotes-To-Remote-Hosting-Prod-Falls-Back-To-Localhost]].
That issue is about where the shell fetches each remote’s remoteEntry.js
asset. This one is about where an already-loaded remote opens its data
socket. Org Workbench proves they are independent: its asset is properly
hosted on Railway and loads fine — then it dials localhost for data.
Why the symptom reads as “no data” rather than “error”
apps/org-workbench/src/App.svelte:38 models the socket as:
let status = $state<'connecting' | 'open' | 'closed' | 'error' | 'auth_required'>('connecting');
The closed badge in the corner is that state, faithfully rendered. But the
main pane does not branch on it — it keeps showing the neutral instructional
copy, “Pick an organization from the coverage roster on the left (fewest corpus
items first), or search above…”, inviting the operator to use a roster that can
never populate. The UI tells the truth in a 60px pill and lies in the 1200px
region next to it. See [[Live-Not-Live-Indicator-Tooling-And-Cross-Service-Error-Surfacing]]
and [[No-User-Visibility-Into-State-Needs-A-State-Inspector]].
The fix
- Replace the constant in all sixteen apps with the env-reading form. This
is mechanical and identical everywhere;
chatis the template to copy. - Delete the dead constant in
corpora-curator/src/App.svelte:9so it cannot be picked up by accident. - Set
PUBLIC_WS_URL=wss://ws.augment.didi.sh/wson theorg-workbench,search-and-add, andsearch-resultsRailway services, then rebuild —PUBLIC_*is baked at build time, sorailway redeploy --service <name> --from-sourceis required. A restart will not do it. - Make the empty state honest — when
statusisclosed/error, the main pane should say the connection failed, not invite a roster pick. - Guard it so it cannot regress. Two cheap options: extend
scripts/design-drift.mjs(or add a sibling lint) with a rule banning literalws://localhostoutside a fallback expression, or assert on it in the test harness named in [[No-Test-Coverage-TDD-Deferred-Despite-Agentic-Fit]]. Without a guard this returns the next time a remote is scaffolded by copy-paste.
Suggested verification
Per the browser-drive discipline in CLAUDE.md, the click-path is: load
https://augment.didi.sh, sign in, switch workspace to Reach Edu, open the
Org Workbench flow, and assert the connection pill reads open and the
coverage roster renders ≥1 organization. That drive currently fails at the pill
and is the regression test for this fix.
Related
- [[Move-Remaining-Remotes-To-Remote-Hosting-Prod-Falls-Back-To-Localhost]] — sibling deployment defect, different axis
- [[Search-And-Add-Invokes-Never-Reach-The-Workspace]] — same flow; worth re-checking whether its prod symptom is actually this
- [[Domain-Type-Is-Ambient-State-So-A-Failed-Workspace-Load-Hides-Every-Corpus]] — the same failure-hidden-behind-a-neutral-empty-state shape
- [[A-Failed-Deploy-Is-Silent-Nothing-Watches-Production-After-Merge]] — why this survived undetected on prod
- [[Live-Not-Live-Indicator-Tooling-And-Cross-Service-Error-Surfacing]]