-
Notifications
You must be signed in to change notification settings - Fork 0
chore(deps): KEEP-1371 bump next from 16.0.10 to 16.1.5 #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
6fa292a to
4be0b81
Compare
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
Workflow Package Upgrade RequiredThe Next.js 16.1.5 upgrade is incompatible with To fix this, I upgraded the workflow package to Breaking Changes in workflow@4.1.0However, the preview deploy shows workflows getting stuck (trigger node stays in "running" state). This is likely due to breaking changes in the new version:
Next StepsNeed to investigate:
Reference: workflow@4.1.0-beta.51 release notes |
|
Bump is incompatible with the current vercel workflow package, and upgrading it causes breaking changes - will need to circle back to this imo. Upstream haven't patched their next version either. Both keeperhub and workflow_builder_template are vulnerable to the next.js CVEs. They haven't upgraded the upstream to the latest vercel/workflow package. The upstream vercel-labs/workflow-builder-template is on next@16.0.10 and is vulnerable to: These are the same security issues that the dependabot PR #229 fixes by upgrading to next@16.1.5. |
Bumps [next](https://github.com/vercel/next.js) from 16.0.10 to 16.1.5. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](vercel/next.js@v16.0.10...v16.1.5) --- updated-dependencies: - dependency-name: next dependency-version: 16.1.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
75f2bd0 to
3cb0c95
Compare
Workflow Package 4.1.0-beta.51 Breaking Change FixProblemThe Root Cause// Current (broken) - lines 42-50 and 98-106
start(executeWorkflow, [{ nodes, edges, triggerInput, executionId, workflowId }]);
// Promise floats, errors lost, workflow may not start// Required (4.1.0-beta.51)
const run = await start(executeWorkflow, [args]);
// run.runId available for trackingFiles to Modify1.
|
| Decision | Choice | Rationale |
|---|---|---|
| Use Run.runId? | Log only, don't store | Our executionId is primary, schema change not worth it |
| Store Run object? | No | We have our own status tracking |
| API response change? | No | Would break clients |
| Error handling | Keep existing | Already updates execution to error state |
Phase 1: Refactor
Address the issue mentioned above the proposed changes. Then I can go ahead and....
Phase 2: Verification
Run the full test suite:
pnpm check # Lint
pnpm type-check # TypeScript
pnpm test:unit # Unit tests
pnpm test:integration # Integration tests
pnpm build # Production buildPhase 3: Manual Testing
- Create a test workflow with multiple steps
- Execute via API endpoint
- Verify execution logs show correct status
- Test webhook trigger
- Verify codegen output includes correct imports
Phase 4: Deployment
- Deploy to staging environment
- Run smoke tests
- Monitor for errors in Sentry
- Deploy to production
- Monitor workflow execution metrics
Version Considerations
| Version | Pros | Cons |
|---|---|---|
| 4.1.0-beta.51 | Already tested in PR, CI passes | Not latest |
| 4.1.0-beta.52 | Fixes Bun circular dependency | May introduce untested changes |
Recommendation: Start with 4.1.0-beta.51. Consider upgrading to beta.52 in a follow-up PR if Bun compatibility is needed.
Rollback Plan
If issues are discovered post-deployment:
- Revert the commit:
git revert <commit-hash> - Deploy the revert
- Investigate issues in staging environment
Note: The security vulnerabilities primarily affect self-hosted deployments. Vercel-hosted applications have WAF protection as an interim measure.
Monitoring
Post-deployment, monitor:
- Workflow execution success rate
- API response times for
/api/workflow/*/execute - Memory usage (CVE fixes relate to DoS via memory exhaustion)
- Build times (withWorkflow plugin performance)
- Sentry for new error patterns
References
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
🧹 PR Environment Cleaned UpThe PR environment has been successfully deleted. Deleted Resources:
All resources have been cleaned up and will no longer incur costs. |
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
🧹 PR Environment Cleaned UpThe PR environment has been successfully deleted. Deleted Resources:
All resources have been cleaned up and will no longer incur costs. |
…cing pnpm strict mode keeps transitive deps in .pnpm/ where Next.js standalone output tracing globs cannot reach them. The .pnpm/ glob approach causes symlink-vs-directory conflicts during copy. Add .npmrc with public-hoist-pattern to promote the full dependency tree of @workflow/world-postgres to top-level node_modules, and list every transitive package in serverExternalPackages and outputFileTracingIncludes so the standalone build includes them.
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
Fix: MODULE_NOT_FOUND for pg-boss (and all world-postgres transitive deps)ProblemPR deploys were failing with Root CauseThree layers of issues compounding:
Why It Worked Locally
Fix (commits
|
| File | Import | Already hoisted? |
|---|---|---|
dist/index.js |
pg-boss |
no |
dist/index.js |
postgres |
yes (direct dep) |
dist/queue.js |
@vercel/queue |
no |
dist/queue.js |
@workflow/world-local |
no |
dist/queue.js |
ulid |
no |
dist/storage.js |
@workflow/errors |
no |
dist/storage.js |
@workflow/world |
no |
dist/storage.js |
ulid |
no |
dist/drizzle/*.js |
cbor-x |
no |
dist/drizzle/*.js |
drizzle-orm |
yes (direct dep) |
@workflow/world-local |
async-sema |
no |
@workflow/world-local |
@workflow/utils |
no |
pg-boss |
cron-parser |
yes (but included for safety) |
Verified
Docker build with --target builder completes with zero Failed to copy traced files errors.
Note on Vercel SDK Design
The dynamic require(env_var) pattern works seamlessly on Vercel's platform where they control the runtime. @workflow/world-postgres is the self-hosted escape hatch — the standalone tracing gap is a known pain point for Docker/K8s deployments. All @workflow/* packages come from https://github.com/vercel/workflow.
@vercel/queue imports @vercel/oidc and mixpart — another layer of transitive deps invisible to the standalone tracer.
|
Whack-a-mole continues — @vercel/queue imports @vercel/oidc and mixpart |
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
postgres, drizzle-orm, zod, dotenv are direct app deps that nft traces from the app code path — but serverExternalPackages prevents nft from seeing them as deps of world-postgres too. Module resolution from inside the externalized package can't find them in standalone.
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
Walk the full dependency tree (34 hard deps) instead of discovering them one runtime error at a time. Generated by reading package.json dependencies recursively from @workflow/world-postgres through the pnpm store. Includes pg and its sub-packages (pg-pool, pg-protocol, etc.), undici, luxon, and all other leaf deps.
🚀 PR Environment DeployedYour PR environment has been successfully deployed! Environment Details:
Components:
The environment will be automatically cleaned up when this PR is closed or merged. |
Walks the full dependency tree of @workflow/world-postgres and outputs the package lists needed for .npmrc and next.config.ts standalone tracing.
🧹 PR Environment Cleaned UpThe PR environment has been successfully deleted. Deleted Resources:
All resources have been cleaned up and will no longer incur costs. |
Production was missing WORKFLOW_TARGET_WORLD, WORKFLOW_POSTGRES_URL, and workflow-postgres-setup in the init container. Without these, prod defaults to Local World (filesystem-based) instead of Postgres World. Aligns prod with staging and PR environment configuration.
KEEP-1371: Next.js 16.0.10 -> 16.1.5 UpgradeSummaryPR #229 upgrades Next.js from 16.0.10 to 16.1.5 (security patches) and bumps the Vercel Workflow SDK from 4.0.1-beta.17 to 4.1.0-beta.51 (breaking change). This document covers the deployment architecture relevant to validating the PR, what the PR environment can and cannot test, and the changes made. Security CVEs AddressedBreaking Change: Workflow SDK
|
| File | Change |
|---|---|
app/api/workflow/[workflowId]/execute/route.ts |
start(...) -> const run = await start(...) |
app/api/workflows/[workflowId]/webhook/route.ts |
Same |
The outer executeWorkflowBackground() function is intentionally NOT awaited in the POST handler (fire-and-forget pattern preserved). Only the inner start() call needed the fix.
SDK Local World: .workflow-data/ Filesystem Requirement
The Problem
The start() function relies on the SDK's "World" system for storage and queuing. On EKS (not Vercel), the SDK defaults to the Local World (@workflow/world-local), which persists run state as JSON files in .workflow-data/runs/.
On SDK 4.0.1-beta.17, start() was fire-and-forget. The filesystem write to .workflow-data/ would fail silently as an unhandled promise rejection — the workflow still executed because it runs in-process. On 4.1.0-beta.51, start() is awaited, so the ENOENT error propagates and kills the execution before the workflow runs:
Error: ENOENT: no such file or directory, open '.workflow-data/runs/wrun_....json.tmp...'
In the K8s container, this directory didn't exist and the non-root nextjs user (uid 1001) couldn't create it under /app (owned by root).
The Fix
Added mkdir -p .workflow-data/runs && chown -R nextjs:nodejs .workflow-data in the Dockerfile runner stage, before the USER nextjs switch. Also added .workflow-data/ to .gitignore for local dev.
Why executeWorkflow() Can't Be Called Directly
Investigated bypassing start() entirely — calling executeWorkflow() directly, the same pattern used by scripts/workflow-runner.ts. This does not work in the Next.js context. The "use workflow" directive is a compile-time transformation: Next.js replaces the function body with a guard that throws if called directly:
Error: You attempted to execute workflow executeWorkflow function directly.
To start a workflow, use start(executeWorkflow) from workflow/api
The workflow-runner script avoids this because it runs via tsx outside the Next.js compiler — the "use workflow" directive is not processed.
Local World Limitations (Resolved: Now Using Postgres World)
The Local World is documented as "designed for development, not production":
- In-memory queue — steps don't persist across server restarts
- Filesystem storage — JSON files in
.workflow-data/ - Single instance — cannot handle distributed deployments
This has been resolved by switching to the Postgres World (WORKFLOW_TARGET_WORLD=@workflow/world-postgres). The Helm values for both staging and PR environments set this env var, and instrumentation.ts initializes the postgres world at startup. The .workflow-data/ directory fix above is still needed as a fallback (the SDK creates it regardless of world backend), but run state is now persisted in PostgreSQL via pg-boss. See the "Standalone Output Tracing" section below for the dependency management this required.
World Configuration Reference
| Env Var | Purpose | Default |
|---|---|---|
WORKFLOW_TARGET_WORLD |
Select world backend | Auto-detect (Local in non-Vercel) |
WORKFLOW_LOCAL_DATA_DIR |
Local World data directory | .workflow-data/ |
WORKFLOW_LOCAL_BASE_URL |
Local World base URL | http://localhost:{port} |
WORKFLOW_LOCAL_QUEUE_CONCURRENCY |
Max concurrent queue workers | 100 |
PR Environment Architecture
PR environments deploy to AWS EKS in an isolated namespace (pr-${PR_NUMBER}) via the deploy-pr-environment label.
What Gets Deployed
| Component | Image Target | Deployed in PR? | Notes |
|---|---|---|---|
| Next.js app | runner |
Yes | Single replica via Helm |
| DB Migrator | migrator |
Yes | Init container, runs db:push + db:seed |
| PostgreSQL | - | Yes | Isolated CNPG cluster per PR |
| LocalStack (SQS) | - | Yes | Emulated SQS queue |
| Schedule Dispatcher | scheduler |
No | Image built but no CronJob deployed |
| Job Spawner | scheduler |
No | Image built but no Deployment deployed |
| Workflow Runner | workflow-runner |
No | Image built but nothing spawns K8s Jobs |
Why Scheduler/Events Are Missing
The PR Helm values template (deploy/pr-environment/values.template.yaml) only defines:
- A single Deployment (the app)
- An init container (the migrator)
It does not include:
- A CronJob for the schedule dispatcher
- A Deployment for the job spawner
- Any connection to the events service or MCP service
The external service API keys (SCHEDULER_SERVICE_API_KEY, EVENTS_SERVICE_API_KEY, MCP_SERVICE_API_KEY) are configured and pulled from staging Parameter Store, but the services that use those keys to call the PR app are not pointing at the PR environment -- they point at the staging app.
Infrastructure Per PR
Namespace: pr-${PR_NUMBER}
├── Deployment: keeperhub-pr-${PR_NUMBER} (Next.js app)
│ └── Init Container: db-migration (migrator)
├── Service: keeperhub-pr-${PR_NUMBER} (ClusterIP :3000)
├── Ingress: app-pr-${PR_NUMBER}.keeperhub.com
├── PostgreSQL: keeperhub-pr-${PR_NUMBER}-db-rw
├── LocalStack: localstack.pr-${PR_NUMBER} (:4566)
│ └── SQS Queue: keeperhub-workflow-queue
└── ServiceAccount: keeperhub-pr-${PR_NUMBER}
└── RBAC: batch/jobs (create,get,list,watch,delete), pods (get,list,watch), pods/log (get)
Execution Paths and PR Testability
Path 1: Manual Test Run (Testable)
User clicks "Run" in UI
-> POST /api/workflow/[workflowId]/execute (session auth)
-> Creates workflowExecutions record
-> await start(executeWorkflow, [...]) <-- THE BREAKING CHANGE
-> Returns executionId
This is the primary path affected by the SDK upgrade. It runs entirely within the Next.js app process. Fully testable in PR environment.
Path 2: Webhook Trigger (Testable)
External HTTP request
-> POST /api/workflows/[workflowId]/webhook
-> Validates webhook config
-> Creates workflowExecutions record
-> await start(executeWorkflow, [...]) <-- THE BREAKING CHANGE
-> Returns executionId
Also runs within the Next.js process. Fully testable in PR environment.
Path 3: Schedule Trigger
CronJob (every minute)
-> schedule-dispatcher queries workflow_schedules (innerJoin workflows, both enabled)
-> Evaluates cron expressions with cron-parser (timezone-aware)
-> Sends SQS message for due workflows
-> Job Spawner polls SQS (long-poll 20s, up to 10 msgs)
-> Validates workflow + schedule still enabled
-> Creates workflowExecutions record (status: "pending")
-> Creates K8s Job (workflow-runner image)
-> scripts/workflow-runner.ts entry point (tsx)
-> Fetches workflow from DB (nodes, edges as JSONB)
-> Validates integration ownership
-> Calls executeWorkflow() DIRECTLY (no start())
-> Updates workflowExecutions + workflowSchedules in DB
Entry point: scripts/workflow-runner.ts (CMD ["tsx", "scripts/workflow-runner.ts"] in Dockerfile workflow-runner target).
SDK surface: The runner does NOT call start() from workflow/api. It calls executeWorkflow() directly from lib/workflow-executor.workflow.ts, which uses "use workflow" and "use step" directives (Workflow DevKit runtime, not the start() API). The start() breaking change in 4.1.0-beta.51 does not affect this path.
Residual risk: The "use workflow" / "use step" directive runtime behavior could theoretically change in the SDK bump (4.0.1 -> 4.1.0), but all 491 unit tests and 91 integration tests pass against executeWorkflow() with the new SDK, which exercises this code path.
Path 4: Internal Service Trigger
MCP / Events / Scheduler service
-> POST /api/workflow/[workflowId]/execute
-> Header: Authorization: Bearer ${SERVICE_API_KEY}
-> authenticateInternalService() validates key
-> Same execution as Path 1
The services that would call this endpoint point at staging, not the PR app.
Validation Strategy
Given the above, the PR should be validated by:
- Unit tests (491 passed) - Core logic
- Integration tests (91 passed) - API routes, DB operations
- E2E tests (11 passed, 1 skipped) - Full UI flows including auth, invitations, org management
- Manual test run in PR environment - Click "Run" on a workflow, verify execution completes
- Webhook test in PR environment - Trigger a webhook-enabled workflow via curl
- Staging deployment - Full validation of all paths including schedules and events
The await start() change is the only code change beyond dependency bumps. It affects Paths 1 and 2 (both testable). Path 3 (schedules) calls executeWorkflow() directly via scripts/workflow-runner.ts -- it never calls start() from workflow/api, so the breaking change does not apply. The same executeWorkflow() function is used by all paths, so test coverage of that function applies regardless of trigger mechanism.
Standalone Output Tracing: MODULE_NOT_FOUND for Workflow World Dependencies
The Problem
PR deploys failed at runtime with MODULE_NOT_FOUND for pg-boss, then @vercel/oidc, then postgres, then undici — each discovered only after deploying. The app worked in local dev (pnpm dev) but broke in Docker/standalone builds.
Root Cause (Three Layers)
-
Dynamic require in the Workflow SDK — The SDK loads the world implementation via
require(process.env.WORKFLOW_TARGET_WORLD), resolving to@workflow/world-postgresat runtime. Next.js standalone output tracing (@vercel/nft) cannot follow dynamic requires where the target is an environment variable, so the package and its entire dependency tree are invisible to the tracer. -
serverExternalPackagesprevents internal tracing — The config listed@workflow/world-postgresinserverExternalPackages, which tells nft "don't look inside this package." So even though@workflow/world-postgres/dist/index.jshasimport PgBoss from 'pg-boss'on line 1, nft never sees it. Every transitive dependency must be explicitly listed. -
pnpm strict mode — Transitive deps live in
.pnpm/and are NOT hoisted to top-levelnode_modules/. TheoutputFileTracingIncludesglobs like./node_modules/pg-boss/**/*match nothing because the package isn't at that path. Attempting.pnpm/path globs causes symlink-vs-directory conflicts (ENOTDIR/ENOENT) because pnpm's internal symlink farms don't survive being copied into standalone.
Why It Works It Development Mode and Not Production Mode
pnpm dev: runs against the fullnode_modules/— everything is there, Node resolves normallynext buildstandalone: produces a minimal.next/standalone/with only traced files — anything nft missed is gone- The bug only manifests in environments running from the standalone output (Docker, PR deploys, production)
The Fix
Two-part solution:
.npmrc with public-hoist-pattern — Forces pnpm to hoist specific transitive deps to top-level node_modules/, making them reachable by the outputFileTracingIncludes globs.
next.config.ts — Lists the full transitive dependency tree (34 packages) in both serverExternalPackages and outputFileTracingIncludes. Generated by walking package.json dependencies recursively from @workflow/world-postgres through the pnpm store (via node scripts/list-world-deps.mjs).
Shared Deps Trap
Even packages the app already uses directly (postgres, drizzle-orm, zod, dotenv) must be listed in outputFileTracingIncludes. nft traces them from the app code, so they exist in standalone — but module resolution from inside @workflow/world-postgres is path-based and may not find them at the expected location in standalone output.
What NOT To Do
- Don't glob into
.pnpm/— symlink farms cause ENOTDIR/ENOENT conflicts during copy - Don't only include the top-level package — its transitive deps won't be traced since the whole package is externalized
- Don't discover deps one deploy at a time — walk the full tree upfront with a script
@vercel/queue Internals
@vercel/queue (0.0.0-alpha.36) is a private Vercel package with no public repo. Source is readable at node_modules/.pnpm/@vercel+queue@*/node_modules/@vercel/queue/dist/index.mjs (~1500 lines). It's a REST queue client for vercel-queue.com with three external imports: @vercel/oidc (OIDC auth tokens), mixpart (multipart stream parsing), and Node builtins. In the postgres world path, it's used mainly for its JsonTransport serializer — the actual queuing goes through pg-boss locally.
Complete Transitive Dependency Tree
All 34 hard dependencies of @workflow/world-postgres (excludes peer deps):
@vercel/oidc @vercel/queue @workflow/errors
@workflow/utils @workflow/world @workflow/world-local
async-sema cbor-x cron-parser
dotenv drizzle-orm luxon
mixpart ms pg
pg-boss pg-connection-string pg-int8
pg-pool pg-protocol pg-types
pgpass postgres postgres-array
postgres-bytea postgres-date postgres-interval
serialize-error split2 type-fest
ulid undici xtend
zod
Sustainability Assessment
This approach requires updating three files (.npmrc, next.config.ts serverExternalPackages, next.config.ts outputFileTracingIncludes) every time the workflow SDK updates. The build succeeds even when deps are missing — failures are runtime-only.
More sustainable alternatives:
node-linker=hoistedin.npmrc— makes pnpm behave like npm, eliminates hoisting issues entirely. Tradeoff: loses pnpm strict isolation.- Dockerfile post-build copy — copy needed packages into standalone after build. Decouples from tracer. Tradeoff: fragile across Next.js versions.
- Upstream fix —
serverExternalPackagesshould recursively include transitive deps. That's a Next.js issue.
Why Vercel Designed It This Way
The dynamic require(env_var) pattern works seamlessly on Vercel's platform where they control the runtime. @workflow/world-postgres is the self-hosted escape hatch. The standalone tracing gap is a known pain point for Docker/K8s deployments but not Vercel's priority use case.
All @workflow/* packages: https://github.com/vercel/workflow
Package Sources
| Package | Repository |
|---|---|
@workflow/* |
https://github.com/vercel/workflow (monorepo) |
@vercel/queue |
Private (no public repo) |
@vercel/oidc |
Private (no public repo) |
pg-boss |
https://github.com/timgit/pg-boss |
cbor-x |
https://github.com/kriszyp/cbor-x |
cbor-extract |
https://github.com/kriszyp/cbor-extract |
ulid |
https://github.com/ulid/javascript |
async-sema |
https://github.com/vercel/async-sema |
cron-parser |
https://github.com/harrisiirak/cron-parser |
mixpart |
Unknown (private) |
Scheduler Service: No Changes Required for Postgres Worlds
Architecture
keeperhub-scheduler (git@github.com:suisuss/keeperhub-scheduler.git) is a separate microservice that evaluates cron schedules and spawns K8s Jobs to execute workflows. It does NOT use the Vercel Workflow SDK or @workflow/world-postgres. Its dependency footprint is minimal and entirely independent:
| Dependency | Purpose |
|---|---|
@aws-sdk/client-sqs |
SQS queue polling |
@kubernetes/client-node |
K8s Job creation |
cron-parser |
Cron expression parsing |
drizzle-orm + postgres |
Direct DB access (shared KeeperHub DB) |
nanoid |
ID generation |
Two-Service Pattern
- Schedule Dispatcher — K8s CronJob, runs every minute, evaluates cron expressions against
workflow_schedulestable, sends SQS messages for due workflows - Job Spawner — Long-running Deployment, polls SQS (long-poll 20s, batch 10), creates
workflow_executionsrecords, spawns K8s Jobs using theworkflow-runnercontainer image
Why No Changes Are Needed
The scheduler's workflow-runner path is completely decoupled from the world system:
| Aspect | Next.js App (Paths 1 & 2) | K8s Runner (Path 3) |
|---|---|---|
| Entry point | Next.js server process | tsx scripts/workflow-runner.ts |
| How workflows execute | await start(executeWorkflow, [...]) |
executeWorkflow() called directly |
| World system | Yes — instrumentation.ts loads postgres world |
No — runner doesn't use start() or any world |
| State persistence | SDK writes via world backend | Runner writes directly to DB via Drizzle |
WORKFLOW_TARGET_WORLD |
Required (set in Helm values) | Not passed, not needed |
| Standalone tracing | Affected (needs all 34 deps hoisted) | Not affected (tsx runs against full node_modules/) |
The "use workflow" directive in lib/workflow-executor.workflow.ts is a compile-time transformation that Next.js applies. When run via tsx, the directive is ignored — executeWorkflow() is callable directly without start().
Env Vars Passed to Runner K8s Jobs
The job-spawner passes these env vars to spawned containers (src/job-spawner.ts:141-152):
WORKFLOW_ID, EXECUTION_ID, SCHEDULE_ID, WORKFLOW_INPUT,
DATABASE_URL, INTEGRATION_ENCRYPTION_KEY
Notably absent: WORKFLOW_TARGET_WORLD, WORKFLOW_POSTGRES_URL. The runner doesn't need them because it doesn't use the SDK's world system at all.
PR Environment Gaps
The PR environment builds the scheduler image but does not deploy it (no CronJob or Deployment in values.template.yaml). Scheduled workflow execution (Path 3) can only be validated in staging or production.
Events Service: No Changes Required
Architecture
keeperhub-events (git@github.com:techops-services/keeperhub-events.git) is a separate microservice that monitors blockchain smart contract events via WebSocket and triggers workflow executions via HTTP. It does NOT use the Vercel Workflow SDK or @workflow/world-postgres. It consists of two services:
| Service | Purpose |
|---|---|
| sc-event-tracker | Spawns child processes per workflow, each opening a WebSocket to an EVM node. Listens for contract events matching the workflow's configured event name, contract address, and ABI. |
| sc-event-worker | Express server (:3010) that periodically fetches active event-triggered workflows from KeeperHub (GET /api/workflows/events?active=true), exposes them to the tracker via /data, and proxies workflow executions to KeeperHub (POST /api/workflow/:id/execute). |
Dependencies
| Package | Purpose |
|---|---|
ethers |
WebSocket provider, ABI parsing, event filtering |
ioredis |
Cross-container process synchronization, transaction deduplication (24h TTL) |
axios |
HTTP calls to KeeperHub API |
express |
Worker HTTP server |
deep-diff |
Detecting workflow configuration changes for process restarts |
Execution Path (Path 4 in this document)
Blockchain emits event
→ sc-event-tracker child process receives log via WebSocket
→ Validates event name match, deduplicates via Redis
→ POST sc-event-worker:3010/workflow/:id/execute { ...eventPayload }
→ Worker wraps as { input: payload }
→ POST KEEPERHUB_API_URL/api/workflow/:id/execute
→ Headers: Authorization: Bearer ${JWT}, X-Internal-Token: ${API_KEY},
X-Service-Key: ${API_KEY}, X-Internal-Execution: true
→ KeeperHub: authenticateInternalService() → await start(executeWorkflow, [...])
Why No Changes Are Needed
The events service is a pure HTTP client to KeeperHub. Every aspect affected by this PR is internal to the Next.js process:
| Aspect | KeeperHub Next.js App | Events Service |
|---|---|---|
| Workflow SDK | Yes — start(), "use workflow", world system |
No — never imported |
await start() breaking change |
Affected (Paths 1, 2, 4) | Not affected — calls HTTP endpoint, not SDK |
| World system (postgres/local) | Yes — instrumentation.ts loads world |
No involvement |
| Standalone output tracing | Affected (34 deps) | Not affected — plain Node.js, no Next.js build |
| HTTP API contract | Unchanged — POST /api/workflow/:id/execute |
Unchanged — same endpoint, same payload shape |
| Response behavior | Unchanged — executeWorkflowBackground() still fire-and-forget in POST handler |
Unchanged — receives same 200 response |
Auth Pattern Note
The events worker authenticates using JWT username/password auth (POST /auth/token) plus X-Internal-Token / X-Service-Key headers with KEEPERHUB_API_KEY. KeeperHub references this key as EVENTS_SERVICE_API_KEY on its side. The auth mechanism was not changed in this PR. If authenticateInternalService() is ever refactored to drop support for these headers in favor of a different pattern, the events worker's http-service.js would need a corresponding update.
PR Environment Gaps
The events service is not deployed in PR environments. Event-triggered workflow execution (Path 4) can only be validated in staging or production.
🧹 PR Environment Cleaned UpThe PR environment has been successfully deleted. Deleted Resources:
All resources have been cleaned up and will no longer incur costs. |
ℹ️ No PR Environment to Clean UpNo PR environment was found for this PR. This is expected if:
|


Bumps next from 16.0.10 to 16.1.5.
Release notes
Sourced from next's releases.
... (truncated)
Commits
acba4a6v16.1.5e1d1fc6Add maximum size limit for postponed body parsing (#88175)500ec83fetch(next/image): reduce maximumResponseBody from 300MB to 50MB (#88588)1caaca3feat(next/image)!: addimages.maximumResponseBodyconfig (#88183)522ed84Sync DoS mitigations for React Flight8cad197[backport][cna] Ensure created app is not considered the workspace root in pn...2718661Backport/docs fixes (#89031)5333625Backport/docs fixes 16.1.5 (#88916)60de6c2v16.1.45f75d22backport: Only filter next config if experimental flag is enabled (#88733) (#...Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)You can disable automated security fix PRs for this repo from the Security Alerts page.