| name | wabi-deploy |
| description | Deploy wabi-server (WabiDB-embedded Rust binary) to Tim or other hosts. Build static frontend + release binary, bind-mount swap, health checks. Cloudflare is optional. Always audit live stack before deploy. |
| version | 3.3.0 |
| author | Hermes Agent |
| license | MIT |
| metadata | {"hermes":{"tags":["deploy","docker","wabi","rust","self-hosted","wabidb","bazzite","cgnat","minimal-compose"],"status":"post-wabidb-cutover-2026-07-17"}} |
Wabi Deploy — WabiDB Single Binary
Hard rule: do not trust this skill blindly
Skills lag the tree. Before any Tim / wabi.chat / remote deploy:
- Read local
docker-compose.yml, core/crates/wabi-server/Dockerfile, and main.rs rust-embed path.
- Probe the live host — mandatory:
references/pre-deploy-live-stack-audit.md.
- If live Tim still has STDB containers and no healthy
wabi-server, that is a stack cutover, not a binary swap. Stop and plan with the user.
- If
wabi-server is already healthy and public https://wabi.chat is 502, that is not cutover and not a binary-swap problem — run references/wabichat-502-diagnosis.md (CF tunnel / client-path routing). Do not “fix CF” by redeploying the origin.
- Never wipe
data/ or uploads/ without explicit user consent.
- Public health must be proven from the user path, not only from the agent’s current egress. Ronin and Tim can disagree (2026-07-17: Tim public 200, Ronin public 502 Host Error while origin stayed green).
Stack simplification (Ronin confirmed 2026-07-17)
With SpacetimeDB fully replaced by WabiDB, the canonical runtime is just wabi-server plus env/proxy. Compose is optional.
Minimal options, in preference order:
- Minimal
docker-compose.yml containing only wabi-server + optional tunnel profiles
- Direct
docker run of the wabi-server image
- Run the local
target/release/wabi-server binary directly on the host
Pick the simplest unless the user explicitly requests a specific path. Do not leave orphan STDB-era compose sections on Tim.
Rust-only validation protocol
When the user says “test the crate,” validate the full embedded-asset path, not just the API:
cargo build --release -p wabi-server --features addons succeeds
- Run locally:
mkdir -p /tmp/wabi-rust-test/data
WABI_JWT_KEY=... WABIDB_ROOT_KEY=... ./target/release/wabi-server \
--data-dir /tmp/wabi-rust-test/data --host 127.0.0.1 --port 3001
- Prove UI serve, not just API:
curl -sS http://127.0.0.1:3001/ | head
Expect <!doctype html> from index.html, not 404. If it 404s, the embedded SPA is broken.
/health and /api/setup/status returning 200 is necessary but not sufficient.
Pre-fix for / returning 404: confirm frontend/build/ contains a proper SvelteKit static output (index.html + _app/), not just adapter-node outputs. Build command must produce static assets for rust_embed to bundle:
cd frontend && rm -rf build .svelte-kit && STATIC_BUILD=1 bun run build
When to use
- Ship a new
wabi-server binary / embedded UI to Tim (or another named host)
- Fresh install path (Minecraft-jar style) on a new box
- Decide CF vs CF-less / CGNAT networking
- Diagnose what is actually running vs what the checkout says
Target architecture (Ronin main / current compose)
Canonical stack is WabiDB only:
| Layer | Tech | Notes |
|---|
| Frontend | SvelteKit static | STATIC_BUILD=1 bun run build → frontend/build/ |
| Backend | Rust Axum wabi-server | bind-mounted ./target/release/wabi-server:/wabi-server:ro |
| Engine | WabiDB in-process | no SpacetimeDB sidecar in current compose |
| Runtime image | FROM fedora:44 | not Debian; verify live image before ABI tricks |
| Data | ./data/wabi-server → /data | never rsync/wipe casually |
| Uploads | ./uploads → /app/uploads | user content |
| Env | WABI_JWT_KEY + WABIDB_ROOT_KEY (64 hex chars) | compose refuses empty JWT; engine needs root key |
| Host port | 3001 → container 3000 | healthcheck is :3000 inside container |
| Tunnels / CF / LiveKit / coturn | compose profiles | optional; not required for core |
Default: docker compose up -d starts only wabi-server.
There is no wabi-backend / wabi-frontend Node stack. STDB containers on a host mean stale checkout.
Networking: CGNAT, calling, Cloudflare
Default calling is WabiDB / Socket.IO relay over the same TCP path as the app (WebSocket). It does not require open UDP ports, STUN/TURN, or a public CF edge to work between clients that can already reach the server.
| Need | CF required? |
|---|
| Users reach server on LAN / Tailscale / port-forward | No |
| Default voice/DM calls (wabidb transport) behind CGNAT | No — only reachability to host:3001 (or HTTPS reverse proxy) |
| Optional P2P / LiveKit SFU | May need TURN/UDP/open ports — not default |
| Pretty public hostname + hide home IP | CF optional |
| Mic/camera in browser on non-localhost | Need secure context: https:// or http://127.0.0.1 / localhost — LAN plain HTTP fails (mediaDevices undefined) |
CF-less is a first-class install path: run wabi-server (optionally Caddy + Let's Encrypt for real HTTPS). Unbind CF when testing that path (stop cloudflared / pause tunnel / DNS). Do not treat CF as the product.
Install path (fresh host)
git clone https://github.com/AzureFoxStudios/wabi.git
cd wabi
openssl rand -base64 48
cat > .env <<'EOF'
WABI_JWT_KEY=<paste openssl rand -base64 48>
WABIDB_ROOT_KEY=<paste openssl rand -hex 32>
WABI_PORT_OUT=3001
TURN_HMAC_KEY=<paste openssl rand -base64 32>
EOF
cd frontend && rm -rf build .svelte-kit && STATIC_BUILD=1 bun run build && cd ..
ls frontend/build/
cargo build --release -p wabi-server --features addons
mkdir -p data/wabi-server uploads plugins
docker compose up -d wabi-server
curl -fsS http://127.0.0.1:3001/health
curl -fsS http://127.0.0.1:3001/api/setup/status
Access: http://127.0.0.1:3001 (calls work).
Public/LAN IP over http:// works for chat but not browser mic until HTTPS.
Update path (existing WabiDB host)
See references/tim-update-runbook.md (commands-only).
Summary:
- Live audit (containers, compose era, health)
STATIC_BUILD=1 frontend build + verify static frontend/build/
cargo build --release -p wabi-server --features addons (addons REQUIRED — without it lore/webhooks routes are silently absent from the binary; if the previous binary also had addons, touch core/addons/lore/backend/src/lib.rs first so the feature-gated crate actually recompiles and the SHA changes)
scp binary to host bind-mount path
docker compose stop wabi-server → rm -f data/wabi-server/.lock → replace binary → docker compose up -d wabi-server (stale lock survives clean SIGTERM; see pitfall 6)
curl host :3001/health, / HTML 200, /api/setup/status, and optional public URL
- Auth smoke (required): no postcard/
user_registered corrupt logs; re-register known owner → 400 taken not 200; wrong-password login → 401. See references/user-record-postcard-compat-and-owner-recovery.md.
Do not docker compose up --build expecting a Rust compile — Dockerfile is runtime-only.
Do not use docker cp into the container — binary is host bind-mount.
Do not probe usernames with live register on production (creates junk accounts).
Tim cutover note (historical; live as of 2026-07-17)
WabiDB cutover on Tim is done when live probe shows healthy wabi-server + WabiDB compose. Orphan STDB containers may still appear in docker ps — do not assume STDB-era solely from orphans; read compose header + health.
If Tim were still STDB-era (no healthy wabi-server): backup → sync tree → WabiDB compose first without tunnel → then Caddy/CF. Binary-swap alone onto STDB-era compose is still wrong. See references/tim-wabidb-cutover-checklist.md.
Tunnel profile (post-cutover CF): dual connectors cloudflared-named + cloudflared-named-2 with --protocol quic (references/wabichat-502-diagnosis.md). Caddy service caddy-tunnel / container wabi-tunnel-caddy on 127.0.0.1:8088; host file Caddyfile.tunnel. WS caveat (pitfall 28): quic tunnels strip the socket.io WS Upgrade header — if the app needs realtime WebSocket, prefer http2 connectors or ship the transports:['websocket','polling'] fallback (pitfall 27); do not add quic connectors to "fix" WS.
Remote hosts (scope exactly what user names)
| Host | SSH | Notes |
|---|
| Tim | root@100.96.11.45 | production WabiDB; public via CF tunnel profiles. Hostname tim does NOT resolve — use the raw IP. |
| Ironin | ironin@100.80.172.12 | ~/Documents/wabi preferred clean tree |
| Iyoku | often unplugged / 100.104.166.42 | staging; skip if user says unplugged |
| Ronin | local | /var/home/Ronin/wabi |
Obey named scope. No drive-by Iyoku.
Build gates and rust-embed UI verification
cd frontend && rm -rf build .svelte-kit && STATIC_BUILD=1 bun run build
ls frontend/build/index.html frontend/build/_app/immutable/assets/
```bash
cd frontend && rm -rf build .svelte-kit && STATIC_BUILD=1 bun run build
ls frontend/build/index.html frontend/build/_app/immutable/assets/
cargo check -p wabi-server
cargo build --release -p wabi-server --features addons
mkdir -p /tmp/wabi-rust-test/data
WABI_JWT_KEY=... WABIDB_ROOT_KEY=... ./target/release/wabi-server \
--data-dir /tmp/wabi-rust-test/data --host 127.0.0.1 --port 3001 &
sleep 6
curl -sS http://127.0.0.1:3001/ | head
curl -sS http://127.0.0.1:3001/health
curl -sS http://127.0.0.1:3001/api/setup/status
kill %1 2>/dev/null || true
If / returns 404, STATIC_BUILD=1 output is wrong/absent — /health alone is NOT proof of embedded assets.
Full-tree sync (code only)
If live container is Fedora 44, prefer building on Fedora/Bazzite. Only use Debian bookworm builder if live image is Debian and Fedora binary crash-loops — see references/tim-glibc-compatible-build.md (historical note).
Full-tree sync (code only)
Never sync data/, uploads/, .env, target/ tree (except shipping the one binary), node_modules/.
rsync -az --delete \
--exclude='data/' --exclude='uploads/' --exclude='.git/' \
--exclude='target/' --exclude='node_modules/' --exclude='frontend/node_modules/' \
--exclude='frontend/.svelte-kit/' --exclude='frontend/build/' \
--exclude='logs/' --exclude='*.log' --exclude='.env' \
./ tim@100.96.11.45:~/Desktop/Wabi/
Then ship binary separately and restart.
Tailscale SSH
Commands (ssh/scp/rsync) to Tim/Iyoku/ironin over Tailscale may hit the web-auth checkpoint and print https://login.tailscale.com/a/<hash>. The agent MUST proactively extract and hand over that raw URL — never just report "blocked"/"denied" and stop (2026-07-18 correction: I reported "blocked" and the user had to ask "supposed to send me the web auth"). Deterministic extraction technique + per-user URL behavior: references/tailscale-ssh-auth.md. After the user authorizes, retry the original command. Even after web-auth clears, scp to a raw 100.x IP triggers a SEPARATE client-side command-approval prompt — a "BLOCKED" there is the consent guard, not Tailscale; re-issue the same command once the user is watching / says send. Do not claim deploy/ship success until the post-auth command actually runs and you verify (e.g. SHA match).
Linked references
references/pre-deploy-live-stack-audit.md — required before Tim/wabi.chat
references/tim-update-runbook.md — commands-only update (WabiDB); lock-clear on every swap
references/user-record-postcard-compat-and-owner-recovery.md — "owner killed" after deploy: postcard UserRecord V1 dual-decode, auth smoke, no register probes, change-password threat model
references/tim-wabidb-cutover-checklist.md — STDB-era Tim → WabiDB cutover (backup, env keys, locks, CF vs origin)
references/wabichat-502-diagnosis.md — public 502: origin-alive, client-IP split, dual cloudflared+quic, SOCKS egress test, compose service names
references/network-cgnat-and-cloudflare.md — CF optional, calling, secure context, CF-less IP hide
references/frontend-deploy-cache-visibility.md — SW / cache headers after UI ship
references/cloudflare-websocket-strip.md — WS upgrade stripped by Cloudflare/cloudflared: curl 101-vs-200 probe, socket.io polling-fallback fix, quic-tunnel caveat
references/tailscale-ssh-auth.md — browser auth flow + deterministic auth-URL extraction technique
references/backup-tarball-handoff.md — archive/handoff only (no deploy): tar working tree incl. uncommitted changes, exclusions, scp to Tim, SHA verify
references/live-stack-verification.md — inventory patterns
Repo human doc: docs/NETWORKING.md (LAN → Tailscale → domain/HTTPS → port-forward).
Historical / archive only (STDB era — do not follow for new deploys)
references/stdb-source-reconciliation.md
references/local-privacy-stdb-podman.md
references/localdev-minimal-2proc.md (STDB-shaped)
references/localdev-reset-and-bot-sandbox.md (STDB reset)
Prefer real WabiDB local-dev: scripts/local-dev.sh / host binary + vite with WABI_JWT_KEY and current compose.
Pitfalls
- Binary swap onto STDB Tim — causes 502 / wrong stack. Audit first. Full recipe:
references/tim-wabidb-cutover-checklist.md.
- Missing
STATIC_BUILD=1 — Node adapter output; rust-embed useless; crash/stale UI.
- Wrong JWT env name — compose wants
WABI_JWT_KEY, not JWT_SECRET / JWT_SIGNING_KEY leftovers in old docs.
- Missing
WABIDB_ROOT_KEY — crash-loop: validation failed for load_bootstrap_key. Need 64 hex chars (openssl rand -hex 32) and list it under environment: so the container receives it (env_file alone is not reliable).
TURN_HMAC_KEY required even when turn profile is off — compose interpolates ${TURN_HMAC_KEY:?…} for coturn; stub any base64 in .env so compose up wabi-server can parse the file.
engine already running after stop/start — WabiDB engine lock is at data/wabi-server/wabidb/.lock (VERIFIED 2026-07-19: removing only the top-level data/wabi-server/.lock did NOT clear the crash-loop "Error: engine already running"; removing the deeper wabidb/.lock did — the runbook already removes it). Proactive on every Tim swap: rm -f data/wabi-server/.lock data/wabi-server/wabidb/.lock between docker compose stop wabi-server and up -d. Do not wait for a crash-loop. (Older skill text claimed the wabidb/.lock path did not exist — that was wrong; this session proved it is the real engine lock.) This also applies to a full docker compose stop → rm → up cycle: a stale wabidb/.lock with no container running still makes the next start crash-loop, so always clear both before up.
- Origin healthy, public CF still 502 — not a binary deploy. Prove
:3001 + :8088 on Tim, then public from Ronin and Tim. Recipe: references/wabichat-502-diagnosis.md. Bypass: http://100.96.11.45:3001.
- Client-IP / path split 502 (2026-07-17) — Tim public can be 200 while Ronin gets CF (~4s, ) with cloudflared log lines for the Ronin IP. SOCKS via Tim → 200. Single is often incomplete. Durable: ( + ) with . Always re-verify the path after tunnel changes.
references/frontend-deploy-cache-visibility.md — expanded: stale-chunk n.subscribe headless repro + stale-token bounce diagnosis.
references/cloudflare-websocket-strip.md — NEW: WS-upgrade-strip diagnosis (curl 101 vs 200 probe), socket.io polling-fallback fix, quic-tunnel caveat.
Production stop/restart
Stopping Tim wabi-server is a live outage. Need user intent for cutover. If the approval UI times out while the user said YOLO/continue, retry once rather than abandoning.
Service worker / cache
After frontend deploys, verify hashed assets and cache headers (references/frontend-deploy-cache-visibility.md). Health 200 alone does not prove UI updated.