Deployment Modes
Fabric and the legacy OptimalOS Pi stack are now the same bundle, served from two origins, gated at runtime. This is the architectural shift that landed on 2026-05-05 (commit bf69427, Option A).
Why two modes
Until tonight, the Pi optimalos.service was running from /home/oracle/.openclaw/workspace/optimalOS — the exact same checkout used to build Fabric — and the Hetzner deploy bundle came from an rsync of that same dist/. So every pnpm build on clenis was contaminating optimal.miami with Fabric routes (vault, fuel meter, Sessions tab) the moment the service restarted.
Carlos wanted optimal.miami to stay as a stable backup of the legacy command center while the clenis branch absorbed Fabric churn. Two checkouts would have meant two divergent codebases to keep in sync. Option A is the cheaper fix: ship one bundle, decide what's live at request-time based on hostname (client) and an environment variable (server).
The two modes
Legacy mode — optimal.miami (Pi)
What lives here:
- Home / Board / Loom / Settings tabs
- tmux session attach +
[XFER]button - All the OpenClaw / Discord / Loom workflow plumbing the Pi already ran
/api/system/*,/api/loom/*,/api/openclaw/*routes (auth-gated as before)
What's gone:
- No Sessions tab
- No fuel meter (top-of-screen throughput / mission / yield)
- No
/vault/*routes —/api/vault/*returns 410 GONE - No Cockpit chrome
- No
/api/auth/setup-init,/api/auth/devices/*,/api/fuel/*— all 410
Fabric mode — fabric.optimal.miami (Hetzner)
Everything legacy mode has, plus:
- Sessions tab in the top bar
- Fuel meter pinned at
top: 44px(cleared the legacy[XFER]button — Phase 14-4) /vault/*routes (setup wizard, dashboard, Add Entry drawer)/api/vault/*,/api/auth/setup-init,/api/auth/devices/*,/api/fuel/*- Cockpit terminal lock-in (Phase 14-5)
- Back-to-OptimalOS button on
/vault/dashboard
How the gate works
┌─────────────────────────────────┐
│ ONE bundle (dist/) │
│ built once on clenis │
└──────────────┬──────────────────┘
│
rsync │ rsync
┌───────────────────┐ │ ┌───────────────────┐
│ Pi (legacy) │ ◀────────┴────────▶ │ Hetzner (fabric) │
│ /home/oracle/... │ │ /opt/optimalos/ │
│ no DEPLOYMENT │ │ DEPLOYMENT= │
│ │ │ hetzner-cloud │
└────────┬──────────┘ └────────┬──────────┘
│ │
▼ ▼
optimal.miami fabric.optimal.miami
(hostname check fails → (hostname matches →
no fabric UI) fabric UI rendered)Server-side gate
The check lives in src/server.ts:
const FABRIC_DEPLOYMENT = process.env.DEPLOYMENT === "hetzner-cloud";
if (FABRIC_DEPLOYMENT) {
app.use("/api/vault", vaultRouter);
app.use("/api/auth/setup-init", setupInitRouter);
app.use("/api/auth/devices", devicesRouter);
app.use("/api/fuel", fuelRouter);
}When FABRIC_DEPLOYMENT is false (the Pi has no DEPLOYMENT env var), those route mounts never happen. Express falls through to the catch-all that returns 410 GONE for any /api/(vault|auth/setup-init|auth/devices|fuel)/* path.
Client-side gate
client/fabric-mode.ts exports isFabricMode():
const FABRIC_HOSTNAMES = new Set([
"fabric.optimal.miami",
]);
export function isFabricMode(): boolean {
if (typeof window === "undefined") return false;
if ((window as any).__FABRIC_MODE__ === true) return true;
if ((window as any).__FABRIC_MODE__ === false) return false;
const host = window.location.hostname;
if (FABRIC_HOSTNAMES.has(host)) return true;
if (host.startsWith("fabric.")) return true;
return false;
}client/main.ts calls isFabricMode() inside showApp() and conditionally renders the Sessions tab, fuel meter, and vault routes. The window.__FABRIC_MODE__ override is the escape hatch for testing or future "promote a non-fabric.* hostname" scenarios.
What's gated where
| Surface | Route / Element | Mode |
|---|---|---|
Home, Board, Loom, Settings, tmux attach, [XFER] | UI tabs | Both |
/api/system/*, /api/loom/*, /api/openclaw/* | server routes | Both |
/api/version | server route | Both (returns 200 everywhere) |
| Sessions tab | top-bar UI | Fabric only |
| Fuel meter | top: 44px overlay | Fabric only |
/vault/setup, /vault/dashboard | client routes | Fabric only |
/api/vault/* | server routes | Fabric only (Pi → 410) |
/api/auth/setup-init | server route | Fabric only (Pi → 410) |
/api/auth/devices/* | server routes | Fabric only (Pi → 410) |
/api/fuel/* | server routes | Fabric only (Pi → 410) |
| Cockpit chrome | client lock-in | Fabric only |
Verified live on 2026-05-05:
optimal.miami:
GET /api/version → 200
GET /api/vault/recipients → 410 GONE
GET /api/auth/setup-init → 410 GONE
GET /api/fuel/state → 410 GONE
GET /api/system/tmux → 401 (auth-gated, route exists)
fabric.optimal.miami:
GET /api/version → 200
GET /api/vault/recipients → 401 (auth-gated, route exists)
GET /api/fuel/state → 401 (auth-gated, route exists)Adding a new fabric origin
Say you want fabric.example.com to also serve fabric mode. Two changes:
Client — add the hostname to the set in
client/fabric-mode.ts:tsconst FABRIC_HOSTNAMES = new Set([ "fabric.optimal.miami", "fabric.example.com", // new ]);Or rely on the existing
fabric.*wildcard.Server — set
DEPLOYMENT=hetzner-cloudin the new origin's env (e.g./opt/optimalos/secrets.env) and restartoptimalos.service.
That's it. ~2 line client change + 1 env var. No code branches, no separate bundle.
Adding a new fabric-only feature
Pattern (Phase 14-4 fuel meter is the worked example):
Server — add the route mount inside the existing
if (FABRIC_DEPLOYMENT)block insrc/server.ts:tsif (FABRIC_DEPLOYMENT) { // ...existing mounts app.use("/api/fuel", fuelRouter); // 14-4 }Client — guard the UI render with
isFabricMode():tsimport { isFabricMode } from "./fabric-mode"; if (isFabricMode()) { mountFuelMeter(document.getElementById("fuel-slot")!); }Test on the Pi — confirm the route returns 410 and the UI element doesn't render. If it does, the gate is leaking.
Troubleshooting
Vault routes work on optimal.miami — DEPLOYMENT was leaked to the Pi env. Check the running process:
pgrep -f optimalos | xargs -I{} sudo tr '\0' '\n' < /proc/{}/environ | grep DEPLOYMENT
# Expect: empty output. If you see DEPLOYMENT=hetzner-cloud, find where
# it leaked in (systemd unit Environment=, /etc/profile, ~/.bashrc, etc.)Vault routes 410 on fabric.optimal.miami — DEPLOYMENT not set on Hetzner. Check:
ssh root@178.156.203.234 'grep DEPLOYMENT /opt/optimalos/secrets.env'
# Expect: DEPLOYMENT=hetzner-cloud
# If missing, add it, then: systemctl restart optimalosFuel meter visible on optimal.miami — bundle is stale on the Pi or the hostname check failed. Hard-refresh (Ctrl+Shift+R), then in devtools console:
window.location.hostname // should NOT match fabric.* on the Pi
window.__FABRIC_MODE__ // should be undefinedIf window.location.hostname is correct but the meter still shows, the bundle is older than bf69427 — re-deploy.
Source pointers
src/server.ts—FABRIC_DEPLOYMENTflag + gated route mountsclient/fabric-mode.ts—isFabricMode(),FABRIC_HOSTNAMES, override seamclient/main.ts—showApp()conditional renders- Commit
bf69427— Option A landing commit (2026-05-05) - Sister page: Architecture → Deployment modes