The upstream v2 routers mount under /v1 without collisions. flowweb
answers /api with 404 instead of the SPA shell, and a CI test fails
on any first-party /api/ or /v2/ path in src/.
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
The backend mounts its API at /v1 and /v2, but flowweb forwarded /api/,
so every API call from the UI came back as the SPA shell. A routing test
covers API paths, SPA routes and the method guard.
Co-authored-by: Hanzo Dev <[email protected]>
One binary fronts flow.hanzo.ai instead of the split flow-site landing + a
separate app host + a disabled-ingress backend. flowweb go:embeds the built flow
UI (Vite frontend) plus a self-contained Hanzo Flow landing and serves it with
SPA fallback, while reverse-proxying /api, /health, /openapi.json, /docs, /logs
etc. to the flow backend (FLOW_BACKEND_URL, default 127.0.0.1:7860). One image,
one origin — hanzoai/world + hanzoai/cloud's embedded-SPA pattern.
At plain `go build` it ships the committed landing placeholder; the image build
overwrites core/cmd/flowweb/frontend/ with the real Vite bundle before `go build`.
Verified: go build clean, GET / serves the landing (200), /api/* proxies.
Docs corpus (Langflow->Hanzo Flow, upstream URLs langflow-ai/langflow->hanzoai/
flow, docs.langflow.org->docs.hanzo.ai), starter-project template help-URLs, CI
workflows, and test files. Remaining 'langflow' repo-wide is now dominated by
LANGFLOW_* env-var names + langflow_ settings fields — the config-boundary layer
deliberately preserved pending a coordinated FLOW_/HANZO_ alias-with-fallback
(renaming them blind breaks every deployment).
The seam the whole platform composes on. A stdlib-only Go module (core/) that
holds zero domain knowledge and is what every node-pack and execution backend
plugs into. flow is curried over two parameters:
Value type — what travels on an edge (Message / Tensor / connector I/O). The
engine never inspects it; a Step interprets it per its domain.
Mode — execution locality/durability (InProcess | GPUResident | Durable),
a PARAMETER not baked in: media flows keep latents pinned in VRAM
and serialize nothing; automations checkpoint every step on the
durable backend. Same scheduler, different Backend.
So every product is flow applied:
studio = Run(GPUResident, graph-of-Tensor-steps) // comfy nodes
automations = Run(Durable, graph-of-ConnectorIO) // triggers -> actions
llmFlow = Run(InProcess, graph-of-Message-steps) // langflow (lfx) nodes
Contract: Value (opaque, typed by PortType), Port/Step (typed in/out ports + Run),
Graph (Add/Wire/Validate — port typecheck + acyclicity), Mode + Backend (the
locality/durability seam), and Scheduler (the ONE topology walk, curried over
value-domain and Mode). Ships with the reference InProcess backend; GPUResident
(studio VRAM) and Durable (auto/tasks activities) register the same way.
Tests prove it: a domain-agnostic 2-step pipeline runs in order, port type
mismatch is rejected at wire time, AnyType bypasses, cycles are caught by
Validate, and Mode selects the backend over an identical scheduler walk.
Next: studio and langflow delete their private executors and become node-packs
on this scheduler; auto's tasks engine registers as the Durable backend.