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 fabric.optimal.miami with Fabric routes (vault, fuel meter, Sessions tab) the moment the service restarted.

Carlos wanted fabric.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 — fabric.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   │
        └────────┬──────────┘                     └────────┬──────────┘
                 │                                         │
                 ▼                                         ▼
        fabric.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:

fabric.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 fabric.optimal.miami — DEPLOYMENT 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.miami — DEPLOYMENT 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 fabric.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.ts — FABRIC_DEPLOYMENT flag + gated route mounts
  • client/fabric-mode.ts — isFabricMode(), FABRIC_HOSTNAMES, override seam
  • client/main.ts — showApp() conditional renders
  • Commit bf69427 — Option A landing commit (2026-05-05)
  • Sister page: Architecture → Deployment modes

Built by Carlos Lenis in Miami