Skip to content

[nest] Fix NestJS Vercel build output#2988

Open
VaguelySerious wants to merge 8 commits into
mainfrom
peter/nest-vercel-support
Open

[nest] Fix NestJS Vercel build output#2988
VaguelySerious wants to merge 8 commits into
mainfrom
peter/nest-vercel-support

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes #1472 and #878

NestJS currently produces no .vercel/output, so nothing emits a queue-consumer function with experimentalTriggers, and Vercel Queue never dispatches the run.

Every other server framework (Express/Fastify/Hono) sidesteps this by being a Nitro app whose Vercel preset emits the consumer. NestJS has its own compiler/DI and can't ride that path, so it needs to emit the Build Output itself.

Approach

Keep nest build (SWC compiles decorators + emitDecoratorMetadata natively). After it, workflow-nest build --vercel (also auto-detected via the VERCEL env var) writes a complete .vercel/output:

  • Consumer — the combined flow.func (+ webhook/[token].func, static manifest, routing) is produced by the shared VercelBuildOutputAPIBuilder, i.e. the exact code path Nitro/Next/etc. use, so it's registered with the __wkf_workflow_* experimentalTriggers and VQS discovers it. Single combined trigger — matches current arch.
  • App — the NestJS app (from the SWC-compiled dist/) is bundled as a catch-all function, with the workflow world statically injected (createWorkflowWorldTargetEsbuildPlugin) so start() works inside it.
  • Routing — merged into config.json: workflow routes + filesystem win, then fall through to the app.

WorkflowModule now lazy-loads the build toolchain (@workflow/builders, esbuild, SWC) so importing it no longer drags the compiler into the runtime serverless bundle. The runtime entry (workflow/nest) no longer re-exports the builders; they live at workflow/nest/builder and workflow/nest/vercel-builder.

This deliberately avoids the earlier #1472 hacks (base64-embedding bundles into source, /tmp writes, globalThis injection, separate step/flow triggers) — each now has a clean shared equivalent.

Testing

  1. Tarballed the SDK, scaffolded a throwaway NestJS app consuming the tarballs, deployed prebuilt to Vercel (team peter-w).
  2. POST /api/startwrun_…; the run completed (both steps + a sleep ran):
    { "status": "completed",
      "returnValue": { "hello": "Hello, vercel!", "bye": "Goodbye, vercel.", "completedAt": "" } }
    i.e. it no longer stays pending.
  • App-function bundle contains no build tooling (esbuild/SWC/@workflow/builders) and boots under VERCEL=1.
  • flow.func/.vc-config.json carries the __wkf_workflow_* queue trigger.
  • @workflow/nest unit tests pass (37).

Status / notes

  • Draft: NestJS is still labelled experimental; this makes Vercel deploys actually work and documents the flow. No E2E Vercel lane for Nest is wired yet — worth adding before calling it supported.
  • The Nest-app bundling uses an esbuild externals allowlist for optional Nest peers; @vercel/nft-style tracing is a more robust future option if the allowlist proves brittle.

Deployed NestJS workflows previously stayed `pending` forever: nothing emitted
a queue-consumer function for Vercel Queue to discover, so runs were never
dispatched. This adds a Vercel Build Output path for NestJS.

`workflow-nest build --vercel` (also triggered when the VERCEL env var is set)
now writes `.vercel/output`:
- the combined workflow queue-consumer `flow.func` is produced by the shared
  `VercelBuildOutputAPIBuilder` — the same code path Nitro/Next/etc. use — so
  it is registered with `experimentalTriggers` and discovered by VQS;
- the NestJS app is bundled (from the SWC-compiled `dist/`) as a catch-all
  function with the workflow world statically injected;
- routing is merged into `config.json` (workflow routes + filesystem win, then
  fall through to the app).

`WorkflowModule` now lazy-loads the build toolchain (@workflow/builders,
esbuild, SWC) so importing it no longer pulls the compiler into the runtime
serverless bundle. The builders remain available via the
`workflow/nest/builder` and `workflow/nest/vercel-builder` subpaths.

Verified end-to-end: deployed a NestJS app to Vercel and a triggered workflow
ran to completion (previously stuck pending).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6e6f9ab

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

This PR includes changesets to release 16 packages
Name Type
@workflow/nest Major
workflow Patch
@workflow/world-testing Patch
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/astro Patch
@workflow/nuxt Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite 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

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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 Jul 18, 2026 1:23am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 18, 2026 1:23am
example-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-astro-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-express-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-fastify-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-hono-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-nitro-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workbench-vite-workflow Ready Ready Preview, Comment Jul 18, 2026 1:23am
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 18, 2026 1:23am
workflow-swc-playground Ready Ready Preview, Comment Jul 18, 2026 1:23am
workflow-tarballs Ready Ready Preview, Comment Jul 18, 2026 1:23am
workflow-web Ready Ready Preview, Comment Jul 18, 2026 1:23am

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 6e6f9ab failed. See the run logs for details.

commit f549c8e · Fri, 17 Jul 2026 20:47:00 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Avg (ms) P10 (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1446 (+14%) 1750 🔴 1892 🔴 2518 🔴 30
TTFS hook + stream 1540 (+16%) 1896 🔴 2120 🔴 2596 🔴 30
STSO 1020 steps (1-20) 285 (-4.5%) 315 🔴 391 🔴 448 🔴 19
STSO 1020 steps (101-120) 288 (-6.5%) 308 🔴 366 🔴 370 🔴 19
STSO 1020 steps (1001-1020) 557 (-11%) 571 🔴 642 🔴 719 🔴 19
WO stream 1446 (+14%) 1750 1892 2518 30
WO hook + stream 1540 (+16%) 1896 2120 2596 30
SL stream 4237 (+324%) 5649 🔴 5697 🔴 5817 🔴 30
SL hook + stream 3765 (+93%) 5520 🔴 5564 🔴 6027 🔴 30
📜 Previous results (1)

dad0793

Fri, 17 Jul 2026 20:03:00 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P10 (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1321 (+4.3%) 1629 🔴 1677 🔴 1788 🔴 30
TTFS hook + stream 1533 (+15%) 1913 🔴 1963 🔴 2142 🔴 30
STSO 1020 steps (1-20) 265 (-11%) 295 🔴 364 🔴 423 🔴 19
STSO 1020 steps (101-120) 280 (-9.4%) 301 🔴 355 🔴 392 🔴 19
STSO 1020 steps (1001-1020) 578 (-7.6%) 614 🔴 778 🔴 837 🔴 19
WO stream 1321 (+4.3%) 1629 1677 1788 30
WO hook + stream 1533 (+15%) 1913 1963 2142 30
SL stream 4495 (+350%) 5794 🔴 5836 🔴 5922 🔴 30
SL hook + stream 4020 (+106%) 5584 🔴 5627 🔴 5721 🔴 30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1452 1 230 1683
✅ 💻 Local Development 1617 0 219 1836
✅ 📦 Local Production 1617 0 219 1836
✅ 🐘 Local Postgres 1617 0 219 1836
✅ 🪟 Windows 153 0 0 153
✅ 📋 Other 894 0 177 1071
✅ vercel-multi-region 27 0 0 27
Total 7377 1 1064 8442

❌ Failed Tests

▲ Vercel Production (1 failed)

express (1 failed):

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
✅ example 126 0 27
❌ express 125 1 27
✅ fastify 126 0 27
✅ hono 126 0 27
✅ nextjs-turbopack 150 0 3
✅ nextjs-webpack 150 0 3
✅ nitro 126 0 27
✅ nuxt 126 0 27
✅ sveltekit 145 0 8
✅ vite 126 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 25
✅ e2e-local-dev-tanstack-start- 128 0 25
✅ e2e-local-postgres-nest-stable 128 0 25
✅ e2e-local-postgres-tanstack-start- 128 0 25
✅ e2e-local-prod-nest-stable 128 0 25
✅ e2e-local-prod-tanstack-start- 128 0 25
✅ e2e-vercel-prod-tanstack-start 126 0 27
✅ vercel-multi-region
App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

Comment thread packages/nest/src/index.ts
…` meta-package, breaking the documented `import { NestLocalBuilder } from "workflow/nest"` and leaving no working alternative.

This commit fixes the issue reported at packages/nest/src/index.ts:6

## The bug

This PR changed `packages/nest/src/index.ts` from a value+type export:

```ts
export { type NestBuilderOptions, NestLocalBuilder } from './builder.js';
```

to a type-only export:

```ts
export type { NestBuilderOptions } from './builder.js';
```

The rationale is valid — `builder.ts` imports `@workflow/builders` (esbuild/SWC) at top level, and the runtime entry must stay free of build-time deps so a NestJS app can be bundled into a serverless function. The `NestLocalBuilder` value is instead intended to be reached via the `workflow/nest/builder` subpath (as the new comment in `index.ts` states).

The problem is that the escape hatch was only half-wired:

- `@workflow/nest`'s own `package.json` *does* expose a `./builder` subpath (`./dist/builder.js`).
- But the recommended consumer package is the **meta `workflow` package**, whose `package.json` only exposes `./nest` → `./dist/nest.js`, and `packages/workflow/src/nest.ts` is just `export * from '@workflow/nest'` (the runtime entry that no longer value-exports `NestLocalBuilder`). There was **no** `./nest/builder` subpath in the meta package.

### Concrete trigger

A user following the API-reference docs writes:

```ts
import { NestLocalBuilder } from "workflow/nest";
```

(`docs/content/docs/v5/api-reference/workflow-nest/nest-local-builder.mdx` line 15 and the v4 equivalent.) At runtime this resolves to `@workflow/nest`'s main entry, which now exports only the *type* `NestBuilderOptions` — so `NestLocalBuilder` is `undefined`, and `new NestLocalBuilder(...)` throws `TypeError: NestLocalBuilder is not a constructor`. There was also no `workflow/nest/builder` subpath to fall back to, so the value class was completely unreachable through the `workflow` meta package.

## The fix

1. Added a `./nest/builder` subpath to `packages/workflow/package.json` mapping to `./dist/nest-builder.js`.
2. Created `packages/workflow/src/nest-builder.ts` = `export * from '@workflow/nest/builder'`, mirroring the existing `src/nest.ts` pattern, so the value export is threaded through the meta package and compiled to `dist/nest-builder.js`.
3. Updated the v4 and v5 `nest-local-builder.mdx` docs to the working import path `import { NestLocalBuilder } from "workflow/nest/builder"`.

This restores reachability of `NestLocalBuilder` for meta-package consumers while preserving the PR's goal of keeping build-time deps out of the runtime entry.

Note: the sandbox has no installed `node_modules`, so a full `tsc` build could not be executed; the change is structurally identical to the already-working `./nest` subpath wiring.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
@VaguelySerious VaguelySerious changed the title [nest] Support deploying NestJS workflows to Vercel [nest] Fix NestJS Vercel build output Jul 17, 2026
@VaguelySerious
VaguelySerious marked this pull request as ready for review July 17, 2026 19:46
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code owners July 17, 2026 19:46
Comment thread docs/content/docs/v5/getting-started/nestjs.mdx Outdated
Comment thread packages/nest/src/vercel-builder.ts Outdated
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Comment thread .changeset/nest-vercel-support.md Outdated
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
'@swc/core/*',
'@swc/wasm',
'esbuild',
'*.node',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: *.node is still always externalized, but this builder does not trace or copy external files into appFuncDir; the emitted function contains only index.js, package.json, and .vc-config.json. Any Nest app dependency with a native addon therefore keeps a runtime reference to a .node file that is absent from the deployed .func, causing MODULE_NOT_FOUND on cold start. Please trace/copy native artifacts (and their package layout) into the function, or fail the build with a clear unsupported-dependency error instead of producing a broken deployment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed as a documented limitation rather than adding native-module tracing, which is out of scope for this exploratory integration (reliably tracing/copying .node artifacts also requires handling the runtime resolution these modules do via bindings/node-gyp-build).

*.node stays externalized so esbuild does not fail the build on a .node file it cannot bundle; I added a comment at that line making the intentional choice and its consequence explicit (b047a12), and called out the limitation in the changeset-linked README section ("Deploying to Vercel") plus the changeset. If native-addon support is needed we can follow up with @vercel/nft tracing.

Comment thread .changeset/nest-vercel-support.md Outdated
@@ -0,0 +1,5 @@
---
'@workflow/nest': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: This is marked as a minor change, but the PR removes the public NestLocalBuilder value export from the @workflow/nest and workflow/nest main entries. Existing documented imports stop working; adding a new /builder subpath provides a migration target but does not preserve compatibility. Because this bump type also governs stable backports, please either retain a compatible main-entry export or mark the change as major.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Bumped @workflow/nest to major (b047a12). Retaining the main-entry NestLocalBuilder export is not an option — it would re-drag the build toolchain (@workflow/builders, esbuild, SWC) into the runtime bundle of every app that imports WorkflowModule, which is the bug this PR fixes — so the subpath (@workflow/nest/builder) is the migration target and the changeset now documents it as a breaking change.

Comment thread packages/nest/src/cli.ts

Commands:
init Generate .swcrc configuration with the workflow plugin
build Build workflow bundles (and the Vercel Build Output when on Vercel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: The package README still documents only the init CLI command and has no Vercel deployment/build instructions. Repository standards require corresponding README updates when package functionality changes, and direct @workflow/nest consumers otherwise have no package-local documentation for this new command. Please update packages/nest/README.md with build, its options, the Vercel entry module, and the vercel-build flow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated packages/nest/README.md (b047a12): added a "Deploying to Vercel" section (serverless entry module, skipBuild on Vercel, the vercel-build flow, native-addon limitation), documented the build command with an options table (--vercel, --dirs, --entry, --out-dir, --module), and noted the NestLocalBuilder/NestVercelBuilder subpaths in "How It Works".

…ive-addon limitation

- Changeset: bump @workflow/nest to major (removing NestLocalBuilder from the
  main entry is a breaking change) and document the subpath migration + the
  native-addon limitation.
- README: document the `build` command and its options, the Vercel entry
  module and `vercel-build` flow, the NestVercelBuilder subpath, and the
  native-addon limitation.
- vercel-builder: comment why `*.node` is externalized and that native addons
  are not yet traced/copied into the deployed function.
- Minor wording: Workflow DevKit -> Workflow SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/nest/README.md Outdated
Comment thread packages/nest/README.md Outdated
```typescript
// _vercel/entry.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ai review: This entry bypasses the nest build output by importing ../src/app.module, so the Vercel builder feeds the Nest TypeScript graph back through esbuild. That contradicts the builder assumption that it bundles already-compiled JS, and esbuild does not emit the TypeScript emitDecoratorMetadata that Nest constructor injection relies on; apps with injected providers can therefore boot with missing DI metadata. Please import ../dist/app.module.js here, matching the checked-in workbench and the v5 guide, so the SWC-compiled decorator metadata is retained.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed in 6e6f9ab. The entry example now imports the compiled ../dist/app.module.js (produced by nest build, whose SWC pass emits emitDecoratorMetadata) instead of raw ../src/app.module, and adds import 'reflect-metadata'. This matches the checked-in workbench/nest/_vercel/entry.ts and the getting-started docs, both of which already used dist/ + reflect-metadata.

…t + reflect-metadata

The entry snippet imported AppModule from raw `../src/app.module` and omitted
`import 'reflect-metadata'`. Importing raw TS routes the app back through esbuild
(which does not emit `emitDecoratorMetadata`), breaking NestJS constructor
injection; the entry must import the `nest build` output (`../dist/app.module.js`)
and register reflect-metadata — matching the workbench entry and getting-started
docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants