Skip to content

Migrate @workflow/web from Next.js to React Router v7#1005

Merged
TooTallNate merged 16 commits into
mainfrom
nate/migrate-web-to-react-router
Feb 13, 2026
Merged

Migrate @workflow/web from Next.js to React Router v7#1005
TooTallNate merged 16 commits into
mainfrom
nate/migrate-web-to-react-router

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate commented Feb 11, 2026

Summary

  • Replace Next.js App Router with React Router v7.13.0 framework mode (Vite-based), eliminating the large next dependency from the web, CLI, and workflow metapackages
  • Serve the web UI in-process from the CLI via Express instead of spawning next start as a child process
  • Switch RPC transport from JSON to CBOR to preserve binary data types across the wire
  • Replace nuqs URL state management with React Router's useSearchParams
  • Replace Next.js server actions with an RPC resource route (/api/rpc) and a thin CBOR-based client

Motivation

The next package is ~300MB installed and was the single largest dependency in the monorepo. It also required spawning a separate child process from the CLI to run the o11y web server, adding complexity around process lifecycle management, port readiness polling, and environment variable forwarding.

With React Router framework mode, the web package builds to a standard Express-compatible server bundle that the CLI can import and serve directly in its own process.

What changed

Framework swap (@workflow/web):

  • next.config.ts / postcss.config.mjsreact-router.config.ts / vite.config.ts
  • src/ directory → app/ directory (React Router convention)
  • src/app/layout.tsx + layout-client.tsxapp/root.tsx
  • src/app/page.tsxapp/routes/home.tsx
  • src/app/run/[runId]/page.tsxapp/routes/run-detail.tsx
  • Path alias @/~/
  • Removed all 'use client' / 'use server' directives

Data transport:

  • Server actions → RPC resource route at /api/rpc with CBOR encoding
  • CBOR preserves Uint8Array and other binary types natively (no base64 overhead)
  • Stream reading → dedicated /api/stream/:streamId resource route

URL state:

  • nuqs (useQueryState) → useSearchParams from react-router

Fonts:

  • next/font/google → Geist .woff2 files referenced directly from node_modules/geist via @font-face in CSS

CLI integration (@workflow/cli):

  • import('@workflow/web/server').then(m => m.startServer(port))
  • No child process, no readiness polling, no cleanup handlers

Radix UI compatibility:

  • onSubmit preventDefault on AlertDialogContent and SheetContent to prevent Radix's internal <form method="dialog"> from triggering React Router route actions
  • Catch-all action on root route for any stray POSTs

Dependencies removed

  • next, swr, nuqs, @tailwindcss/postcss

Dependencies added

  • react-router / @react-router/dev / @react-router/node / @react-router/express (all 7.13.0)
  • express, vite, @tailwindcss/vite, cbor-x, isbot, cross-env
  • geist (devDep)

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 11, 2026

🦋 Changeset detected

Latest commit: ca37040

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@workflow/web Patch
@workflow/cli Patch
workflow Patch
@workflow/world-testing Patch
@workflow/core Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 11, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 490 0 38 528
✅ 💻 Local Development 418 0 62 480
✅ 📦 Local Production 418 0 62 480
✅ 🐘 Local Postgres 418 0 62 480
✅ 🪟 Windows 45 0 3 48
❌ 🌍 Community Worlds 103 41 9 153
✅ 📋 Other 123 0 21 144
Total 2015 41 257 2313

❌ Failed Tests

🌍 Community Worlds (41 failed)

turso (41 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 44 0 4
✅ example 44 0 4
✅ express 44 0 4
✅ fastify 44 0 4
✅ hono 44 0 4
✅ nextjs-turbopack 47 0 1
✅ nextjs-webpack 47 0 1
✅ nitro 44 0 4
✅ nuxt 44 0 4
✅ sveltekit 44 0 4
✅ vite 44 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 45 0 3
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
✅ mongodb 45 0 3
✅ redis-dev 3 0 0
✅ redis 45 0 3
✅ turso-dev 3 0 0
❌ turso 4 41 3
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 41 0 7
✅ e2e-local-postgres-nest-stable 41 0 7
✅ e2e-local-prod-nest-stable 41 0 7

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 11, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.033s (-1.2%) 1.005s (~) 0.972s 10 1.00x
💻 Local Next.js (Turbopack) 0.033s (-18.8% 🟢) 1.005s (~) 0.972s 10 1.01x
💻 Local Express 0.034s (+33.6% 🔺) 1.005s (~) 0.971s 10 1.02x
🌐 Redis Next.js (Turbopack) 0.040s (-13.9% 🟢) 1.005s (~) 0.965s 10 1.22x
🌐 MongoDB Next.js (Turbopack) 0.075s (-21.9% 🟢) 1.007s (~) 0.932s 10 2.28x
🐘 Postgres Nitro 0.154s (+16.9% 🔺) 1.009s (~) 0.856s 10 4.65x
🐘 Postgres Express 0.171s (+73.9% 🔺) 1.010s (~) 0.838s 10 5.19x
🐘 Postgres Next.js (Turbopack) 0.363s (-20.2% 🟢) 1.010s (~) 0.647s 10 11.00x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.680s (-25.4% 🟢) 1.895s (-15.3% 🟢) 1.215s 10 1.00x
▲ Vercel Express 0.687s (-21.8% 🟢) 2.160s (-0.8%) 1.473s 10 1.01x
▲ Vercel Nitro 0.739s (-3.0%) 1.904s (-15.9% 🟢) 1.165s 10 1.09x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.079s (-2.2%) 2.006s (~) 0.926s 10 1.00x
🌐 Redis Next.js (Turbopack) 1.086s (-1.4%) 2.006s (~) 0.920s 10 1.01x
💻 Local Nitro 1.106s (-0.5%) 2.006s (~) 0.900s 10 1.02x
💻 Local Express 1.113s (+3.6%) 2.006s (~) 0.893s 10 1.03x
🌐 MongoDB Next.js (Turbopack) 1.296s (-2.0%) 2.008s (~) 0.712s 10 1.20x
🐘 Postgres Next.js (Turbopack) 1.569s (-14.6% 🟢) 2.012s (~) 0.442s 10 1.45x
🐘 Postgres Nitro 2.247s (-8.1% 🟢) 3.014s (~) 0.767s 10 2.08x
🐘 Postgres Express 2.293s (-7.3% 🟢) 3.013s (~) 0.720s 10 2.12x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.571s (-9.2% 🟢) 3.630s (-7.2% 🟢) 1.060s 10 1.00x
▲ Vercel Express 2.638s (-7.0% 🟢) 3.676s (-13.1% 🟢) 1.037s 10 1.03x
▲ Vercel Next.js (Turbopack) 3.047s (+4.6%) 3.962s (-1.2%) 0.915s 10 1.19x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 10.545s (-1.7%) 11.022s (~) 0.478s 3 1.00x
💻 Local Next.js (Turbopack) 10.571s (-1.7%) 11.022s (~) 0.451s 3 1.00x
💻 Local Nitro 10.823s (-0.7%) 11.023s (~) 0.200s 3 1.03x
💻 Local Express 10.888s (+3.1%) 11.022s (~) 0.134s 3 1.03x
🌐 MongoDB Next.js (Turbopack) 12.210s (-0.6%) 13.019s (~) 0.809s 3 1.16x
🐘 Postgres Next.js (Turbopack) 15.140s (+0.7%) 16.046s (+3.2%) 0.906s 2 1.44x
🐘 Postgres Express 20.439s (~) 21.056s (~) 0.617s 2 1.94x
🐘 Postgres Nitro 20.456s (~) 21.057s (~) 0.601s 2 1.94x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 19.085s (-13.4% 🟢) 19.661s (-14.7% 🟢) 0.576s 2 1.00x
▲ Vercel Express 19.129s (-10.4% 🟢) 19.752s (-14.5% 🟢) 0.623s 2 1.00x
▲ Vercel Next.js (Turbopack) 19.715s (-7.8% 🟢) 20.770s (-7.6% 🟢) 1.055s 2 1.03x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 26.546s (-1.4%) 27.052s (~) 0.506s 3 1.00x
💻 Local Next.js (Turbopack) 26.784s (-1.8%) 27.048s (-3.6%) 0.264s 3 1.01x
💻 Local Nitro 27.476s (~) 28.054s (~) 0.578s 3 1.04x
💻 Local Express 27.634s (+3.1%) 28.055s (+3.7%) 0.420s 3 1.04x
🌐 MongoDB Next.js (Turbopack) 30.377s (-0.7%) 31.039s (~) 0.662s 2 1.14x
🐘 Postgres Next.js (Turbopack) 38.289s (+3.7%) 38.595s (+2.7%) 0.306s 2 1.44x
🐘 Postgres Express 50.228s (~) 51.124s (+1.0%) 0.896s 2 1.89x
🐘 Postgres Nitro 50.237s (~) 51.131s (~) 0.894s 2 1.89x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 48.643s (-10.8% 🟢) 49.170s (-12.1% 🟢) 0.527s 2 1.00x
▲ Vercel Next.js (Turbopack) 49.444s (-10.9% 🟢) 50.804s (-10.9% 🟢) 1.360s 2 1.02x
▲ Vercel Express 50.353s (-7.5% 🟢) 51.767s (-8.7% 🟢) 1.413s 2 1.04x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 53.673s (-1.3%) 54.095s (-1.8%) 0.422s 2 1.00x
💻 Local Next.js (Turbopack) 55.636s (-2.0%) 56.097s (-1.8%) 0.460s 2 1.04x
💻 Local Nitro 57.255s (~) 58.104s (~) 0.848s 2 1.07x
💻 Local Express 57.466s (+3.1%) 58.108s (+3.6%) 0.642s 2 1.07x
🌐 MongoDB Next.js (Turbopack) 60.703s (-0.5%) 61.050s (~) 0.347s 2 1.13x
🐘 Postgres Next.js (Turbopack) 73.963s (+13.1% 🔺) 74.671s (+13.7% 🔺) 0.709s 2 1.38x
🐘 Postgres Nitro 98.491s (-1.9%) 99.234s (-2.0%) 0.743s 1 1.84x
🐘 Postgres Express 100.472s (~) 101.216s (~) 0.744s 1 1.87x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 96.892s (-16.3% 🟢) 97.346s (-16.6% 🟢) 0.454s 1 1.00x
▲ Vercel Next.js (Turbopack) 98.905s (-10.5% 🟢) 99.899s (-10.8% 🟢) 0.994s 1 1.02x
▲ Vercel Express 107.657s (-1.2%) 108.289s (-1.9%) 0.632s 1 1.11x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.185s (-6.5% 🟢) 2.006s (~) 0.821s 15 1.00x
💻 Local Next.js (Turbopack) 1.351s (-4.1%) 2.004s (~) 0.653s 15 1.14x
💻 Local Express 1.394s (+2.9%) 2.005s (~) 0.611s 15 1.18x
💻 Local Nitro 1.424s (-0.5%) 2.006s (~) 0.581s 15 1.20x
🐘 Postgres Next.js (Turbopack) 2.090s (+0.5%) 2.397s (-12.5% 🟢) 0.307s 13 1.76x
🌐 MongoDB Next.js (Turbopack) 2.154s (~) 3.007s (~) 0.853s 10 1.82x
🐘 Postgres Express 2.516s (+9.0% 🔺) 3.013s (~) 0.497s 10 2.12x
🐘 Postgres Nitro 2.528s (+17.3% 🔺) 3.013s (+19.9% 🔺) 0.485s 10 2.13x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.684s (-5.4% 🟢) 3.801s (+3.0%) 1.117s 8 1.00x
▲ Vercel Nitro 3.089s (+0.7%) 3.890s (-25.9% 🟢) 0.801s 8 1.15x
▲ Vercel Express 3.468s (+11.7% 🔺) 4.332s (+1.7%) 0.864s 7 1.29x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 2.367s (-5.2% 🟢) 3.007s (~) 0.641s 10 1.00x
💻 Local Next.js (Turbopack) 2.386s (-0.8%) 3.007s (~) 0.620s 10 1.01x
💻 Local Nitro 2.551s (-0.7%) 3.008s (~) 0.457s 10 1.08x
💻 Local Express 2.601s (+13.5% 🔺) 3.008s (~) 0.407s 10 1.10x
🌐 MongoDB Next.js (Turbopack) 4.656s (-2.3%) 5.177s (~) 0.521s 6 1.97x
🐘 Postgres Nitro 6.872s (-25.2% 🟢) 7.526s (-21.0% 🟢) 0.654s 4 2.90x
🐘 Postgres Express 7.850s (-8.2% 🟢) 8.527s (-5.6% 🟢) 0.677s 4 3.32x
🐘 Postgres Next.js (Turbopack) 12.698s (-2.1%) 13.035s (-2.5%) 0.337s 3 5.37x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.998s (~) 4.017s (-15.2% 🟢) 1.019s 8 1.00x
▲ Vercel Express 3.833s (+25.5% 🔺) 5.113s (+10.0% 🔺) 1.279s 6 1.28x
▲ Vercel Next.js (Turbopack) 4.332s (+24.8% 🔺) 5.451s (+11.5% 🔺) 1.119s 6 1.45x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 3.877s (-4.4%) 4.134s (-12.5% 🟢) 0.256s 8 1.00x
💻 Local Next.js (Turbopack) 6.530s (~) 7.213s (-4.0%) 0.682s 5 1.68x
💻 Local Express 7.096s (+12.5% 🔺) 7.769s (+10.8% 🔺) 0.673s 4 1.83x
💻 Local Nitro 7.345s (-0.6%) 8.021s (~) 0.676s 4 1.89x
🌐 MongoDB Next.js (Turbopack) 9.962s (~) 10.349s (-3.2%) 0.387s 3 2.57x
🐘 Postgres Express 47.122s (-1.0%) 48.117s (~) 0.995s 1 12.15x
🐘 Postgres Nitro 52.033s (+5.3% 🔺) 52.136s (+4.0%) 0.103s 1 13.42x
🐘 Postgres Next.js (Turbopack) 52.210s (-7.4% 🟢) 53.128s (-7.0% 🟢) 0.918s 1 13.47x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.263s (+19.0% 🔺) 7.731s (+15.9% 🔺) 1.468s 5 1.00x
▲ Vercel Express 7.160s (+55.1% 🔺) 8.317s (+38.5% 🔺) 1.156s 4 1.14x
▲ Vercel Next.js (Turbopack) 7.385s (+26.2% 🔺) 8.359s (+11.4% 🔺) 0.974s 4 1.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.214s (-5.0%) 2.006s (~) 0.792s 15 1.00x
💻 Local Next.js (Turbopack) 1.396s (-0.8%) 2.005s (~) 0.609s 15 1.15x
💻 Local Express 1.425s (+5.0% 🔺) 2.006s (~) 0.580s 15 1.17x
💻 Local Nitro 1.440s (+1.9%) 2.005s (~) 0.565s 15 1.19x
🐘 Postgres Nitro 1.961s (-1.8%) 2.397s (-7.8% 🟢) 0.435s 13 1.62x
🐘 Postgres Next.js (Turbopack) 2.020s (-6.7% 🟢) 2.596s (-11.2% 🟢) 0.576s 12 1.66x
🌐 MongoDB Next.js (Turbopack) 2.137s (-1.3%) 3.008s (~) 0.871s 10 1.76x
🐘 Postgres Express 2.350s (+15.2% 🔺) 3.013s (+9.9% 🔺) 0.663s 10 1.94x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.581s (-14.4% 🟢) 3.408s (-23.4% 🟢) 0.827s 10 1.00x
▲ Vercel Express 2.662s (-5.7% 🟢) 3.377s (-12.9% 🟢) 0.715s 9 1.03x
▲ Vercel Nitro 2.977s (+5.1% 🔺) 3.734s (-7.4% 🟢) 0.757s 9 1.15x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 2.379s (-6.7% 🟢) 3.008s (~) 0.629s 10 1.00x
💻 Local Next.js (Turbopack) 2.529s (+3.1%) 3.007s (~) 0.478s 10 1.06x
💻 Local Express 2.648s (+10.6% 🔺) 3.007s (~) 0.359s 10 1.11x
💻 Local Nitro 2.688s (+1.7%) 3.008s (~) 0.320s 10 1.13x
🌐 MongoDB Next.js (Turbopack) 4.800s (~) 5.176s (-3.2%) 0.376s 6 2.02x
🐘 Postgres Express 8.409s (-30.5% 🟢) 8.778s (-29.0% 🟢) 0.368s 4 3.54x
🐘 Postgres Nitro 10.231s (-7.7% 🟢) 11.030s (-8.4% 🟢) 0.799s 3 4.30x
🐘 Postgres Next.js (Turbopack) 12.763s (-10.5% 🟢) 13.367s (-7.0% 🟢) 0.604s 3 5.37x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.729s (-14.8% 🟢) 4.113s (-8.2% 🟢) 1.384s 8 1.00x
▲ Vercel Nitro 2.809s (-7.6% 🟢) 4.006s (-12.1% 🟢) 1.197s 8 1.03x
▲ Vercel Next.js (Turbopack) 3.402s (+11.8% 🔺) 4.016s (-1.6%) 0.614s 8 1.25x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 3.847s (-6.3% 🟢) 4.133s (-9.8% 🟢) 0.286s 8 1.00x
💻 Local Next.js (Turbopack) 7.052s (-1.1%) 7.515s (~) 0.463s 4 1.83x
💻 Local Express 7.707s (+6.4% 🔺) 8.022s (~) 0.316s 4 2.00x
💻 Local Nitro 7.989s (+2.9%) 8.770s (+9.3% 🔺) 0.781s 4 2.08x
🌐 MongoDB Next.js (Turbopack) 9.825s (-3.7%) 10.348s (-3.2%) 0.523s 3 2.55x
🐘 Postgres Express 45.430s (-12.0% 🟢) 46.094s (-11.6% 🟢) 0.664s 1 11.81x
🐘 Postgres Nitro 52.347s (-2.3%) 53.112s (-1.9%) 0.765s 1 13.61x
🐘 Postgres Next.js (Turbopack) 56.359s (+3.9%) 57.124s (+3.6%) 0.765s 1 14.65x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.438s (+3.8%) 4.399s (-6.8% 🟢) 0.961s 7 1.00x
▲ Vercel Express 4.197s (+10.9% 🔺) 5.431s (+11.0% 🔺) 1.234s 6 1.22x
▲ Vercel Next.js (Turbopack) 5.191s (+17.4% 🔺) 6.440s (+17.0% 🔺) 1.249s 5 1.51x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.109s (-28.6% 🟢) 1.000s (~) 0.001s (-15.4% 🟢) 1.007s (~) 0.898s 10 1.00x
💻 Local Next.js (Turbopack) 0.109s (-25.5% 🟢) 1.001s (~) 0.009s (-8.8% 🟢) 1.014s (~) 0.905s 10 1.00x
💻 Local Nitro 0.174s (-5.7% 🟢) 1.002s (~) 0.011s (+5.7% 🔺) 1.016s (~) 0.843s 10 1.59x
💻 Local Express 0.186s (+68.8% 🔺) 1.003s (~) 0.011s (+10.4% 🔺) 1.016s (~) 0.830s 10 1.71x
🌐 MongoDB Next.js (Turbopack) 0.511s (+1.7%) 0.931s (-1.9%) 0.001s (+9.1% 🔺) 1.008s (~) 0.497s 10 4.68x
🐘 Postgres Next.js (Turbopack) 0.866s (+44.1% 🔺) 0.742s (-24.1% 🟢) 0.001s (-12.5% 🟢) 1.011s (~) 0.144s 10 7.93x
🐘 Postgres Nitro 1.119s (-52.8% 🟢) 1.915s (-24.1% 🟢) 0.001s (-14.3% 🟢) 2.013s (-30.9% 🟢) 0.894s 10 10.24x
🐘 Postgres Express 2.140s (-12.4% 🟢) 2.900s (+11.6% 🔺) 0.001s (+27.3% 🔺) 3.014s (~) 0.874s 10 19.60x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.516s (-10.8% 🟢) 2.928s (-10.9% 🟢) 0.314s (+14.2% 🔺) 3.818s (-10.9% 🟢) 1.301s 10 1.00x
▲ Vercel Express 2.557s (-8.0% 🟢) 2.996s (-10.5% 🟢) 0.564s (+284.4% 🔺) 4.250s (+1.0%) 1.693s 10 1.02x
▲ Vercel Next.js (Turbopack) 2.780s (-2.6%) 3.150s (-12.2% 🟢) 0.235s (-19.5% 🟢) 3.999s (-12.8% 🟢) 1.219s 10 1.10x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 11/12
🐘 Postgres Next.js (Turbopack) 6/12
▲ Vercel Nitro 8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 💻 Local 11/12
Next.js (Turbopack) 🌐 Redis 10/12
Nitro 💻 Local 10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Feb 12, 2026 8:14pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Feb 12, 2026 8:14pm
example-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-astro-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-express-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-fastify-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-hono-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-nitro-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-nuxt-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workbench-vite-workflow Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Feb 12, 2026 8:14pm
workflow-nest Ready Ready Preview, Comment Feb 12, 2026 8:14pm
workflow-swc-playground Ready Ready Preview, Comment Feb 12, 2026 8:14pm

Copy link
Copy Markdown
Member Author

TooTallNate commented Feb 11, 2026

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Feb 11, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​react-router@​7.10.1 ⏵ 7.13.096100 +2379 +198 +1100
Addednpm/​cross-env@​7.0.310010010082100
Addednpm/​express@​4.22.19710010087100
Addednpm/​isbot@​5.1.3510010010094100

View full report

@TooTallNate TooTallNate changed the title web: replace Next.js with React Router v7 scaffolding Migrate @workflow/web from Next.js to React Router v7 Feb 11, 2026
@TooTallNate TooTallNate changed the title Migrate @workflow/web from Next.js to React Router v7 Migrate @workflow/web from Next.js to React Router v7 Feb 11, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request migrates the @workflow/web package from Next.js App Router to React Router v7 framework mode (Vite-based), significantly reducing the dependency footprint and simplifying the CLI integration. The migration eliminates the ~300MB next package and replaces the child process architecture with an in-process Express server that the CLI can import directly.

Changes:

  • Framework migration from Next.js 16 to React Router v7.13.0 with Vite bundler
  • RPC transport switched from JSON (Next.js server actions) to CBOR encoding to preserve binary data types
  • CLI integration simplified from child process spawning to in-process Express server import
  • URL state management migrated from nuqs to React Router's native useSearchParams

Reviewed changes

Copilot reviewed 74 out of 85 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/web/vite.config.ts Vite configuration for React Router with Tailwind and path aliases
packages/web/react-router.config.ts React Router framework configuration
packages/web/tsconfig.json Updated TypeScript config for React Router types and ES2022 target
packages/web/package.json Dependencies updated: removed Next.js/SWR/nuqs, added React Router/Express/CBOR
packages/web/server/app.ts Express app with React Router request handler
packages/web/server.js Production server entry point that CLI imports
packages/web/app/routes.ts File-based routing configuration
packages/web/app/root.tsx Root layout migrated from Next.js layout pattern
packages/web/app/routes/*.tsx Route components migrated from Next.js pages
packages/web/app/routes/api.rpc.tsx CBOR-based RPC API endpoint replacing server actions
packages/web/app/routes/api.stream.$streamId.tsx Stream data resource route
packages/web/app/lib/rpc-client.ts Client-side CBOR RPC implementation
packages/web/app/lib/url-state.ts URL state hooks using React Router's useSearchParams
packages/web/app/lib/types.ts Shared types extracted from server actions
packages/web/app/globals.css Font loading via direct @font-face (removed next/font)
packages/web/app/components/**/*.tsx Import paths changed from @/ to ~/, 'use client' directives removed
packages/cli/src/lib/inspect/web.ts Simplified to import and start server in-process
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

packages/web/app/globals.css:22

  • Font file paths use relative URLs pointing to node_modules, which is fragile and may not work correctly in all deployment scenarios. When the built application is deployed, node_modules is typically not included, and these relative paths will break. Consider using Vite's explicit import syntax (e.g., import fontUrl from 'geist/...') or copying the font files to the public directory during build.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/web/app/routes/api.rpc.tsx
Comment thread packages/web/app/root.tsx
Comment thread packages/cli/src/lib/inspect/web.ts
Comment thread packages/web/app/routes/api.stream.$streamId.tsx Outdated
Comment thread packages/web/app/routes/api.stream.$streamId.tsx Outdated
Comment thread packages/web/server/app.ts Outdated
Comment thread packages/web/package.json Outdated
Comment thread packages/web/app/lib/rpc-client.ts
Comment thread packages/web/app/routes/api.rpc.tsx
Comment thread packages/web/app/routes/api.rpc.tsx
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works outside this repository. Running this in flight-booking-app via tarball, it tells me:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'next-themes' imported from /Users/peter/code/workflow-examples/flight-booking-app/node_modules/.pnpm/@workflow+web@https+++workflow-docs-mn332g6ys.vercel.sh+workflow-web.tgz_react-dom@19.2_7bc1f3d5c367e05b7722a809a4625b5e/node_modules/@workflow/web/build/server/assets/server-build-BNusQVv2.js
    at Object.getPackageJSONURL (node:internal/modules/package_json_reader:316:9)
    at packageResolve (node:internal/modules/esm/resolve:768:81)
    at moduleResolve (node:internal/modules/esm/resolve:858:18)
    at defaultResolve (node:internal/modules/esm/resolve:990:11)
    at #cachedDefaultResolve (node:internal/modules/esm/loader:718:20)
    at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:735:38)
    at ModuleLoader.resolveSync (node:internal/modules/esm/loader:764:52)
    at #resolve (node:internal/modules/esm/loader:700:17)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:35)
    at ModuleJob.syncLink (node:internal/modules/esm/module_job:143:33)

Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add react/react-dom as
peerDependencies

Won't work for non-react projects!

Replace Next.js framework config with React Router v7.13.0 framework mode:
- Add react-router.config.ts, vite.config.ts, server/app.ts, server.js
- Update package.json: swap next/swr/nuqs deps for react-router/express/vite
- Update tsconfig.json: change path alias @/ to ~/, add .react-router types
- Update .gitignore, turbo.json, components.json for new build output
- Delete next.config.ts, postcss.config.mjs (PostCSS handled by Vite)
Rename src/ to app/ (React Router convention) and restructure the app:
- Create app/root.tsx replacing layout.tsx + layout-client.tsx
- Create app/routes.ts with route config for /, /run/:runId, /api/rpc, /api/stream/:streamId
- Create route modules: home.tsx, run-detail.tsx
- Create api.rpc.tsx resource route (RPC proxy replacing server actions)
- Create api.stream.$streamId.tsx resource route (streaming data)
- Create app/lib/rpc-client.ts (fetch wrapper for /api/rpc)
- Create app/lib/types.ts (shared types for server/client boundary)
- Rewrite app/lib/url-state.ts (useSearchParams replacing nuqs)
- Add self-hosted Geist fonts via @font-face in globals.css
- Convert server actions to .server.ts modules (Vite server-only convention)
- Replace all @/ imports with ~/, remove 'use client' directives
- Replace next/link, next/navigation with react-router equivalents
- Replace nuqs useQueryState with useSearchParams
- Update workflow-api-client.ts to use rpc-client instead of server actions
- Delete old Next.js layout, pages, server actions, hooks, config files
Replace the child process spawn of 'next start' with an in-process
Express server imported from @workflow/web/server:
- Dynamically import startServer() from @workflow/web
- Eliminate spawn(), serverProcess, readiness polling, cleanup handlers
- Server is ready immediately when listen() callback fires
- Removes runtime dependency on 'next' being resolvable from CLI
Radix AlertDialog and Sheet render an internal <form method='dialog'>.
Outside a native <dialog> element, browsers default to POST on the
current URL, which React Router interprets as a route action submission.

Fix by preventing default form submission on AlertDialogContent and
SheetContent. Also add safety action exports to route modules that
redirect stray POSTs back to GET.
Remove migration-era language from JSDoc comments in web.ts.
Point @font-face src URLs at ../node_modules/geist/dist/fonts/ so
Vite resolves and bundles them at build time. Removes the committed
.woff2 copies from app/assets/fonts/.
Use import('@workflow/web/server') instead of resolving the package
path with createRequire and constructing a file path manually.
Add server.d.ts for TypeScript support.
Only console.error for genuine server-side failures (5xx). API-level
client errors (4xx), run-not-found errors, and unrecognized errors
from world backends are not logged server-side — the error is returned
to the caller for handling.
Move the stray-POST handling from individual route actions to a
single catch-all action on root.tsx. This covers all current and
future routes without needing per-route action stubs.
Switch the RPC layer from JSON to CBOR encoding to preserve binary
data (Uint8Array) across the wire. CBOR natively handles binary
types without base64 overhead.

Hydration (deserializing input/output/eventData from binary format
to JS values) stays server-side for now. Making @workflow/core's
hydration code browser-safe requires extracting it from the
serialization module which has deep Node.js dependencies (Buffer,
AsyncLocalStorage, child_process, etc.). This will be addressed
as a prerequisite when e2e encryption lands.

- Add cbor-x as runtime dependency
- RPC route: encode responses as CBOR
- RPC client: send/receive CBOR with proper Content-Type headers
- Server actions: hydrate data before CBOR encoding (no JSON
  round-trip needed since CBOR preserves all JS types)
- Hydration errors are caught and return raw data instead of crashing
- Add error handling for malformed CBOR/JSON request bodies (400)
- Remove unused readStreamServerAction import from RPC route
- Validate streamId format and startIndex parameter in stream route
- Decode CBOR error body in RPC client for better error messages
- Remove duplicate static file middleware from server/app.ts
- Fix dev script to use react-router dev instead of node server.js
Vite's SSR build externalizes certain packages instead of bundling
them. When @workflow/web is installed from npm (not in the monorepo),
these externalized packages must be in dependencies to be available
at runtime. Move all Radix UI, lucide-react, class-variance-authority,
clsx, tailwind-merge, date-fns, next-themes, sonner, @xyflow/react,
and workspace packages to dependencies. Add react/react-dom as
peerDependencies.
Use ssr.noExternal=true in Vite config to bundle all dependencies
into the server build. This means @workflow/web only needs express
at runtime — all UI dependencies (Radix, lucide-react, etc.) are
compiled into the server bundle. Keeps the installed package small.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants