Skip to content

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:

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():

ts
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

SurfaceRoute / ElementMode
Home, Board, Loom, Settings, tmux attach, [XFER]UI tabsBoth
/api/system/*, /api/loom/*, /api/openclaw/*server routesBoth
/api/versionserver routeBoth (returns 200 everywhere)
Sessions tabtop-bar UIFabric only
Fuel metertop: 44px overlayFabric only
/vault/setup, /vault/dashboardclient routesFabric only
/api/vault/*server routesFabric only (Pi → 410)
/api/auth/setup-initserver routeFabric only (Pi → 410)
/api/auth/devices/*server routesFabric only (Pi → 410)
/api/fuel/*server routesFabric only (Pi → 410)
Cockpit chromeclient lock-inFabric 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:

  1. Client — add the hostname to the set in client/fabric-mode.ts:

    ts
    const FABRIC_HOSTNAMES = new Set([
      "fabric.optimal.miami",
      "fabric.example.com",   // new
    ]);

    Or rely on the existing fabric.* wildcard.

  2. Server — set DEPLOYMENT=hetzner-cloud in the new origin's env (e.g. /opt/optimalos/secrets.env) and restart optimalos.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):

  1. Server — add the route mount inside the existing if (FABRIC_DEPLOYMENT) block in src/server.ts:

    ts
    if (FABRIC_DEPLOYMENT) {
      // ...existing mounts
      app.use("/api/fuel", fuelRouter);   // 14-4
    }
  2. Client — guard the UI render with isFabricMode():

    ts
    import { isFabricMode } from "./fabric-mode";
    
    if (isFabricMode()) {
      mountFuelMeter(document.getElementById("fuel-slot")!);
    }
  3. 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.miamiDEPLOYMENT was leaked to the Pi env. Check the running process:

bash
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.miamiDEPLOYMENT not set on Hetzner. Check:

bash
ssh root@178.156.203.234 'grep DEPLOYMENT /opt/optimalos/secrets.env'
# Expect: DEPLOYMENT=hetzner-cloud
# If missing, add it, then: systemctl restart optimalos

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

js
window.location.hostname           // should NOT match fabric.* on the Pi
window.__FABRIC_MODE__              // should be undefined

If window.location.hostname is correct but the meter still shows, the bundle is older than bf69427 — re-deploy.

Source pointers

  • src/server.tsFABRIC_DEPLOYMENT flag + gated route mounts
  • client/fabric-mode.tsisFabricMode(), FABRIC_HOSTNAMES, override seam
  • client/main.tsshowApp() conditional renders
  • Commit bf69427 — Option A landing commit (2026-05-05)
  • Sister page: Architecture → Deployment modes

Built by Carlos Lenis in Miami