You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Using Workflow with a Nitro-based framework (Nitro v2/v3, Nuxt) used to run the local observability UI as a separate process: the dev-only /_workflow handler booted the entire @workflow/web Express server on a random port and 302-redirected to it. That second server, second port, and cross-origin redirect is what this PR eliminates.
Now the dashboard is served in-process, on the same origin/port, at a configurable route (default /_workflow) — no second server, no redirect, no orphan process.
This is the first half of making @workflow/nitro an all-in-one integration. The other half — auto-starting the world on boot — is a follow-up PR that builds on #2544 (ensureWorldStarted) and is intentionally not included here.
Changes
@workflow/web — new framework-neutral @workflow/web/handler export (createWorkflowWebHandler({ basename })) that serves SSR + static client assets + RPC as a single Web Request→Response handler under a runtime mount path. The React Router build (Vite base /) is reprefixed at runtime — asset-manifest URLs + publicPath — so the dashboard is self-contained under its mount (<basename>/assets/...), never touching the host app's root namespace. Adds @workflow/web/registry (best-effort discovery) and makes the RPC/stream client basename-aware. The standalone @workflow/web/server (CLI at /) is unchanged.
@workflow/nitro — addDashboardHandler now mounts that handler in-process (Nitro v2 h3.fromWebHandler + v3 native paths) instead of redirecting. New dashboard?: boolean | { enabled?, path? } option; default = on in dev, off in prod. When disabled nothing is registered, so production bundles carry no @workflow/web import (zero bundle/startup cost). Excluded on Vercel deploys.
@workflow/cli — workflow web / inspect --web detect an already-running embedded dashboard (via the registry, health-checked) and open it instead of spawning a redundant server on :3456. --standalone forces the standalone UI.
Because the embedded data layer shares the host process's process.env, it reads the same world as the running app automatically (no extra wiring).
Test plan
Verified manually across all three targets + prod gating + CLI:
Check
Result
workbench/nitro-v3 (v3 native) /_workflow
200 HTML, assets reprefixed to /_workflow/assets/… + served text/javascript, RPC success:true
workbench/nitro-v2 (v2 h3)
200, UI + assets + RPC all working
workbench/nuxt (Vite SSR)
200, UI + assets; deep-link /_workflow/run/x served by the handler (no SPA-fallback issue)
No second port / orphan
only the app's port listens; registry isolates per-pid
Prod build (default)
/_workflow serves the app's own index.html; no @workflow/web in the bundle
CLI coordination
detects embedded URL; --standalone bypasses
Automated
@workflow/web + @workflow/nitro unit tests pass; Biome clean
Follow-up (separate PR, not here)
Auto-start the world on server boot via a Nitro plugin (ensureWorldStarted), gated off Vercel — depends on #2544 merging first.
pranaygp
changed the title
feat(nitro): embed observability dashboard in-process at /_workflow[RFC] feat(nitro): embed observability dashboard in-process at /_workflowJun 20, 2026
Serve the @workflow/web observability UI inside the Nitro process at a
configurable route (default /_workflow) instead of spawning a separate
web server and 302-redirecting to it. Enabled in dev, omitted from
production builds by default (so prod bundles carry no @workflow/web
import). Never mounted on Vercel deploys (use the hosted dashboard).
- @workflow/web: add a framework-neutral `@workflow/web/handler`
(createWorkflowWebHandler) that serves SSR + static client assets +
RPC as one Web Request->Response handler under a runtime basename
(asset manifest URLs + publicPath are reprefixed so the dashboard is
self-contained under its mount). Add `@workflow/web/registry` for
embedded-dashboard discovery; make the RPC/stream client basename-aware.
- @workflow/nitro: mount the handler in-process (Nitro v2 h3 + v3 native
paths), gated by a new `dashboard` option (default = dev).
- @workflow/cli: `workflow web` / `inspect --web` defer to a running
embedded dashboard instead of starting a redundant server; pass
`--standalone` to force the standalone UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback on the embedded dashboard:
- Normalize the dashboard mount path in one place before it feeds both
the Nitro route registration (`[path, path + '/**']`) and the handler
`basename`. Force a single leading slash, strip trailing slashes, and
reject the root mount, so a custom `path` can't make the route and the
handler's internal `normalizeBasename` disagree.
- Replace the handler-level `!nitro.routing` v2 checks with the existing
`isNitroV2()` helper for consistent v2/v3 detection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
AI Review: Note
This adds a user-facing dashboard option, but the PR touches no docs. Two updates needed in docs/content/docs/v5/api-reference/workflow-nitro/index.mdx:
The behavior line still says "Serves a redirect to the local observability dashboard at /_workflow in development" — now stale; it's served in-process with no redirect.
Add dashboard to the module-options table (type, default nitro.options.dev, configurable path).
Per repo convention, user-facing changes should be reflected in docs.
The reason will be displayed to describe this comment to others. Learn more.
AI Review: Note
No automated coverage for the new logic. normalizeDashboardPath is a pure function that's cheap to unit-test and has real edge cases worth pinning: trailing slashes, duplicate leading slashes, and the root-mount rejection that falls back to the default (the /** catch-all swallowing the host app is exactly what this guards against). The handler's asset-reprefixing and the CLI registry-detection path are also untested.
I verified all of these manually (in-process serve, asset reprefix, custom path remount, --standalone bypass, embedded detection), but a couple of unit tests would lock the behavior in against regressions.
The reason will be displayed to describe this comment to others. Learn more.
AI Review: Nit
res.status < 500 treats a 404 at the recorded URL as a live dashboard. The pid-liveness check above handles the common stale case, so this is low-risk, but a process that's alive yet no longer serving that exact mount (e.g. the path was reconfigured) would lead the CLI to open a 404. Consider narrowing to < 400 (a 404 means nothing is serving that route).
This is feature work: it adds a new @workflow/web/handler + @workflow/web/registry export surface, a new dashboard config option on @workflow/nitro, and a new --standalone CLI flag, all carrying minor changesets. Replacing the redirect-to-separate-server approach with an in-process mount is a capability/architecture enhancement, not a fix to a defect on stable.
To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Using Workflow with a Nitro-based framework (Nitro v2/v3, Nuxt) used to run the local observability UI as a separate process: the dev-only
/_workflowhandler booted the entire@workflow/webExpress server on a random port and 302-redirected to it. That second server, second port, and cross-origin redirect is what this PR eliminates.Now the dashboard is served in-process, on the same origin/port, at a configurable route (default
/_workflow) — no second server, no redirect, no orphan process.This is the first half of making
@workflow/nitroan all-in-one integration. The other half — auto-starting the world on boot — is a follow-up PR that builds on #2544 (ensureWorldStarted) and is intentionally not included here.Changes
@workflow/web— new framework-neutral@workflow/web/handlerexport (createWorkflowWebHandler({ basename })) that serves SSR + static client assets + RPC as a single WebRequest→Responsehandler under a runtime mount path. The React Router build (Vite base/) is reprefixed at runtime — asset-manifest URLs +publicPath— so the dashboard is self-contained under its mount (<basename>/assets/...), never touching the host app's root namespace. Adds@workflow/web/registry(best-effort discovery) and makes the RPC/stream client basename-aware. The standalone@workflow/web/server(CLI at/) is unchanged.@workflow/nitro—addDashboardHandlernow mounts that handler in-process (Nitro v2h3.fromWebHandler+ v3 native paths) instead of redirecting. Newdashboard?: boolean | { enabled?, path? }option; default = on in dev, off in prod. When disabled nothing is registered, so production bundles carry no@workflow/webimport (zero bundle/startup cost). Excluded on Vercel deploys.@workflow/cli—workflow web/inspect --webdetect an already-running embedded dashboard (via the registry, health-checked) and open it instead of spawning a redundant server on:3456.--standaloneforces the standalone UI.Because the embedded data layer shares the host process's
process.env, it reads the same world as the running app automatically (no extra wiring).Test plan
Verified manually across all three targets + prod gating + CLI:
workbench/nitro-v3(v3 native)/_workflow/_workflow/assets/…+ servedtext/javascript, RPCsuccess:trueworkbench/nitro-v2(v2 h3)workbench/nuxt(Vite SSR)/_workflow/run/xserved by the handler (no SPA-fallback issue)/_workflowserves the app's ownindex.html; no@workflow/webin the bundle--standalonebypasses@workflow/web+@workflow/nitrounit tests pass; Biome cleanFollow-up (separate PR, not here)
Auto-start the world on server boot via a Nitro plugin (
ensureWorldStarted), gated off Vercel — depends on #2544 merging first.🤖 Generated with Claude Code