Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add World.getEncryptionKeyForRun and thread encryption key through serialization - #979

Merged
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface
Feb 18, 2026
Merged

Add World.getEncryptionKeyForRun and thread encryption key through serialization#979
TooTallNate merged 4 commits into
mainfrom
nate/encryptor-interface

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds World.getEncryptionKeyForRun(run) returning Uint8Array | undefined as the interface for retrieving per-run encryption keys
  • Updates all 8 dehydrate/hydrate serialization functions to accept key: Uint8Array | undefined
  • Updates runtime callers, CLI, and tests to thread the key parameter through

@github-actions

github-actionsBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production534038572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2497452752817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • 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
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4804
✅ example4804
✅ express4804
✅ fastify4804
✅ hono4804
✅ nextjs-turbopack5101
✅ nextjs-webpack5101
✅ nitro4804
✅ nuxt4804
✅ sveltekit4804
✅ vite4804
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

@vercel

vercelBot commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f28311e

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

This PR includes changesets to release 18 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/cliPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
workflowPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

Comment threadpackages/core/src/workflow.test.ts Outdated

@pranaygppranaygp left a comment

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.

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

Comment threadpackages/core/src/runtime/resume-hook.ts
@TooTallNate
TooTallNate marked this pull request as ready for review February 9, 2026 23:12
CopilotAI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate
TooTallNateforce-pushed the nate/encryptor-interface branch from f2a9138 to 78aa5f9CompareFebruary 10, 2026 17:42

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One important non-blocking note on the world interface choice

// Resolve encryption key for the new run. Since start() always runs on the
// current deployment, we can use a placeholder runId — the implementation
// will resolve to the local deployment's key regardless.
const encryptionKey = await world.getEncryptionKeyForRun?.(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this isn't adequately documented in the world interface . If a placeholder "works", then getEncryptionKeyForRun on the world should allow null or something similar and describe how the fallback works.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this.

Comment threadpackages/core/src/runtime/resume-hook.ts

@pranaygppranaygp left a comment

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.

Overall this is a clean, well-structured PR that threads the encryption key through the serialization layer in preparation for at-rest encryption. The interface design on World is good — optional method means no breaking changes for existing implementations. A few comments below, mostly nits and one potential performance concern.

const startTime = Date.now();
const stepEncryptionKey =
await world.getEncryptionKeyForRun?.(workflowRunId);
const dehydrated = await dehydrateStepReturnValue(

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.

The encryption key is resolved twice for the same workflowRunId — once at line 301 for hydration and again here for dehydration. This means two calls to world.getEncryptionKeyForRun?.() per step execution.

Since workflowRunId doesn't change between hydration and dehydration, you could resolve it once before both trace blocks and reuse it:

constencryptionKey=awaitworld.getEncryptionKeyForRun?.(workflowRunId);

Not a correctness issue, but for production deployments where getEncryptionKeyForRun might involve a network call (e.g., KMS), this avoids an unnecessary round-trip.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch. Hoisted the getEncryptionKeyForRun call above both trace blocks so it resolves once and is reused for both hydration and dehydration.

export async function resumeHook<T = any>(
tokenOrHook: string | Hook,
payload: T
payload: T,

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.

nit: _encryptionKey uses the underscore-prefix convention that typically signals an unused parameter, but it is used on lines 106 and 110. Consider renaming to encryptionKeyOverride or just encryptionKey to better communicate intent.

(I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed — renamed to encryptionKeyOverride to properly communicate intent.

runId: string,
_key: Uint8Array | undefined,
ops: Promise<void>[] = [],
global: Record<string, any> = globalThis,

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.

nit: The JSDoc @param list above (lines 1370-1377) is stale — it still references global before runId and doesn't mention the new _key parameter. Since this is an internal API it's not urgent, but updating the params would help future readers. Same applies to the other 7 function JSDocs.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Updated all 8 function JSDoc blocks with accurate @param descriptions matching the current signatures.

* @returns The per-run AES-256 key, or undefined if encryption is not configured
*/
getEncryptionKeyForRun?(
run: WorkflowRun | string

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.

The signature accepts WorkflowRun | string but every callsite I see in the diff passes a string (runId). Is there an existing or planned callsite that passes a full WorkflowRun object? If not, simplifying to just string would reduce the interface surface area. If there is a planned use, a brief note in the JSDoc about when to prefer which form would be helpful.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The WorkflowRun form is used by the o11y/CLI path (in the later PRs in the stack) where the run entity is already fetched and provides the deploymentId for cross-deployment key resolution. Updated the JSDoc to document both use cases.

resource: T,
_encryptorResolver?: unknown
): T {
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;

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.

nit: The type of _encryptorResolver is unknown here, but EncryptionKeyResolver is defined in the sibling output.ts. For consistency and forward-compat, consider importing and using that type so when encryption is wired into the CLI hydration, the type is already correct.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added a proper EncryptionKeyResolver type definition in hydration.ts matching the one in output.ts, and typed the parameter accordingly.

@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.033s (+30.3% 🔺)1.005s (~)0.972s101.00x
💻 LocalNitro0.034s (+5.3% 🔺)1.005s (~)0.972s101.03x
💻 LocalNext.js (Turbopack)0.044s (+8.1% 🔺)1.005s (~)0.961s101.35x
🌐 RedisNext.js (Turbopack)0.049s (+4.5%)1.005s (~)0.956s101.50x
🐘 PostgresNitro0.093s (-18.2% 🟢)1.010s (~)0.917s102.85x
🌐 MongoDBNext.js (Turbopack)0.119s (+33.2% 🔺)1.007s (~)0.888s103.65x
🐘 PostgresExpress0.135s (-69.3% 🟢)1.010s (~)0.874s104.14x
🐘 PostgresNext.js (Turbopack)0.373s1.010s0.636s1011.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.592s (-7.9% 🟢)2.119s (-2.4%)1.527s101.00x
▲ VercelNext.js (Turbopack)0.679s (+23.0% 🔺)2.233s (+20.1% 🔺)1.553s101.15x
▲ VercelExpress0.786s (+19.6% 🔺)2.208s (+1.1%)1.422s101.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.101s (~)2.006s (~)0.904s101.00x
💻 LocalExpress1.107s (+3.3%)2.006s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)1.109s (~)2.006s (~)0.897s101.01x
💻 LocalNitro1.111s (~)2.006s (~)0.895s101.01x
🌐 MongoDBNext.js (Turbopack)1.308s (~)2.008s (~)0.699s101.19x
🐘 PostgresNext.js (Turbopack)1.752s2.012s0.260s101.59x
🐘 PostgresExpress2.376s (+2.5%)3.014s (~)0.638s102.16x
🐘 PostgresNitro2.469s (-1.1%)3.015s (~)0.545s102.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.192s (-8.7% 🟢)3.412s (-7.5% 🟢)1.220s101.00x
▲ VercelNitro2.247s (-4.3%)3.556s (+3.0%)1.309s101.02x
▲ VercelNext.js (Turbopack)2.424s (+7.5% 🔺)3.632s (+21.1% 🔺)1.208s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.718s (-0.5%)11.023s (~)0.305s31.00x
🌐 RedisNext.js (Turbopack)10.735s (~)11.023s (~)0.288s31.00x
💻 LocalExpress10.834s (+2.7%)11.022s (~)0.188s31.01x
💻 LocalNitro10.848s (~)11.023s (~)0.174s31.01x
🌐 MongoDBNext.js (Turbopack)12.271s (-0.7%)13.024s (~)0.752s31.14x
🐘 PostgresNext.js (Turbopack)15.133s16.048s0.915s21.41x
🐘 PostgresNitro20.346s (~)21.061s (~)0.715s21.90x
🐘 PostgresExpress20.425s (+1.3%)21.057s (~)0.632s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.888s (+2.8%)19.304s (+1.2%)1.416s21.00x
▲ VercelNitro17.972s (+3.1%)19.266s (+3.1%)1.294s21.00x
▲ VercelNext.js (Turbopack)18.291s (+1.0%)19.433s (+3.3%)1.142s21.02x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.892s (~)27.051s (~)0.159s31.00x
💻 LocalNext.js (Turbopack)27.224s (~)28.051s (~)0.827s31.01x
💻 LocalExpress27.483s (+2.8%)28.051s (+3.7%)0.569s31.02x
💻 LocalNitro27.595s (~)28.054s (~)0.459s31.03x
🌐 MongoDBNext.js (Turbopack)30.552s (~)31.042s (~)0.490s21.14x
🐘 PostgresNext.js (Turbopack)37.717s38.090s0.373s21.40x
🐘 PostgresExpress50.242s (+0.6%)51.137s (+2.0%)0.894s21.87x
🐘 PostgresNitro50.412s (~)51.134s (~)0.722s21.87x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro267.686s (+541.3% 🔺)269.508s (+524.6% 🔺)1.822s11.00x
▲ VercelNext.js (Turbopack)308.006s (+609.6% 🔺)308.595s (+594.7% 🔺)0.589s11.15x
▲ VercelExpress340.656s (+694.2% 🔺)342.158s (+664.6% 🔺)1.502s11.27x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.297s (~)55.098s (~)0.801s21.00x
💻 LocalNext.js (Turbopack)56.769s (~)57.103s (~)0.335s21.05x
💻 LocalExpress57.603s (+3.6%)58.104s (+3.6%)0.501s21.06x
💻 LocalNitro57.664s (+0.7%)58.106s (~)0.442s21.06x
🌐 MongoDBNext.js (Turbopack)60.971s (~)61.564s (~)0.593s21.12x
🐘 PostgresNext.js (Turbopack)75.129s75.668s0.539s21.38x
🐘 PostgresNitro100.396s (~)101.253s (~)0.857s11.85x
🐘 PostgresExpress100.513s (+33.4% 🔺)101.253s (+32.9% 🔺)0.740s11.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro109.978s (+26.1% 🔺)110.539s (+24.8% 🔺)0.561s11.00x
▲ VercelExpress153.347s (+61.6% 🔺)155.409s (+62.0% 🔺)2.062s11.39x
▲ VercelNext.js (Turbopack)180.557s (+99.9% 🔺)182.256s (+98.6% 🔺)1.699s11.64x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.264s (+1.0%)2.006s (~)0.742s151.00x
💻 LocalNext.js (Turbopack)1.403s (-1.7%)2.005s (~)0.602s151.11x
💻 LocalNitro1.428s (+0.9%)2.005s (~)0.577s151.13x
💻 LocalExpress1.436s (+3.6%)2.006s (~)0.570s151.14x
🐘 PostgresNext.js (Turbopack)1.839s2.011s0.171s151.46x
🌐 MongoDBNext.js (Turbopack)2.165s (+1.0%)3.009s (~)0.844s101.71x
🐘 PostgresNitro2.449s (+7.0% 🔺)3.015s (~)0.566s101.94x
🐘 PostgresExpress2.545s (+22.0% 🔺)3.015s (~)0.470s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.399s (+2.1%)3.671s (-2.1%)1.272s91.00x
▲ VercelNitro2.529s (-8.0% 🟢)3.975s (-1.5%)1.446s81.05x
▲ VercelNext.js (Turbopack)2.982s (+36.3% 🔺)4.135s (+40.1% 🔺)1.153s91.24x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.513s (+0.7%)3.008s (~)0.495s101.00x
💻 LocalNext.js (Turbopack)2.519s (-2.9%)3.007s (~)0.488s101.00x
💻 LocalExpress2.652s (+15.4% 🔺)3.007s (~)0.356s101.06x
💻 LocalNitro2.694s (+3.3%)3.007s (~)0.313s101.07x
🌐 MongoDBNext.js (Turbopack)4.756s (~)5.178s (~)0.421s61.89x
🐘 PostgresNitro7.753s (-8.4% 🟢)8.277s (-5.7% 🟢)0.524s43.08x
🐘 PostgresExpress8.854s (-3.3%)9.282s (-7.4% 🟢)0.429s43.52x
🐘 PostgresNext.js (Turbopack)11.562s12.036s0.474s34.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.607s (+1.1%)3.592s (-2.7%)0.985s91.00x
▲ VercelExpress2.855s (+13.2% 🔺)4.071s (+11.1% 🔺)1.216s81.10x
▲ VercelNext.js (Turbopack)4.044s (+50.6% 🔺)5.073s (+44.3% 🔺)1.028s61.55x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.087s (+1.4%)4.583s (-5.8% 🟢)0.495s71.00x
💻 LocalExpress7.710s (+23.6% 🔺)8.269s (+17.9% 🔺)0.559s41.89x
💻 LocalNext.js (Turbopack)7.799s (+3.0%)8.268s (+3.2%)0.469s41.91x
💻 LocalNitro7.939s (+5.1% 🔺)8.269s (+3.1%)0.330s41.94x
🌐 MongoDBNext.js (Turbopack)9.844s (-1.1%)10.354s (-3.1%)0.511s32.41x
🐘 PostgresNitro44.604s (-3.7%)45.117s (-4.2%)0.513s110.91x
🐘 PostgresExpress46.785s (+1.2%)47.123s (~)0.338s111.45x
🐘 PostgresNext.js (Turbopack)56.922s57.128s0.206s113.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.126s (+68.3% 🔺)7.719s (+60.2% 🔺)1.593s41.00x
▲ VercelExpress6.926s (+136.3% 🔺)8.296s (+112.1% 🔺)1.370s41.13x
▲ VercelNitro7.461s (+17.6% 🔺)8.938s (+13.0% 🔺)1.477s41.22x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.235s (~)2.006s (~)0.771s151.00x
💻 LocalExpress1.437s (+5.7% 🔺)2.005s (~)0.567s151.16x
💻 LocalNext.js (Turbopack)1.449s (-0.9%)2.006s (~)0.557s151.17x
💻 LocalNitro1.452s (+3.6%)2.005s (~)0.552s151.18x
🐘 PostgresExpress1.865s (-14.4% 🟢)2.155s (-19.6% 🟢)0.290s141.51x
🐘 PostgresNitro1.900s (-7.3% 🟢)2.514s (-11.2% 🟢)0.613s121.54x
🐘 PostgresNext.js (Turbopack)2.029s2.512s0.483s121.64x
🌐 MongoDBNext.js (Turbopack)2.162s (+0.9%)3.008s (~)0.846s101.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.274s (~)3.438s (-3.1%)1.165s91.00x
▲ VercelNitro2.326s (-63.3% 🟢)3.387s (-56.4% 🟢)1.061s91.02x
▲ VercelNext.js (Turbopack)3.186s (+47.4% 🔺)4.475s (+49.4% 🔺)1.288s71.40x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)2.486s (~)3.008s (~)0.522s101.00x
💻 LocalNext.js (Turbopack)2.735s (-0.7%)3.108s (~)0.373s101.10x
💻 LocalNitro2.847s (+4.5%)3.008s (~)0.161s101.15x
💻 LocalExpress2.873s (+21.6% 🔺)3.108s (+3.3%)0.234s101.16x
🌐 MongoDBNext.js (Turbopack)4.799s (+2.2%)5.346s (+3.3%)0.547s61.93x
🐘 PostgresExpress9.113s (-25.8% 🟢)9.783s (-23.0% 🟢)0.670s43.67x
🐘 PostgresNitro10.072s (+0.7%)10.701s (~)0.629s34.05x
🐘 PostgresNext.js (Turbopack)12.955s13.367s0.411s35.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.322s (+32.7% 🔺)5.582s (+41.7% 🔺)1.261s61.00x
▲ VercelNitro4.923s (+93.0% 🔺)6.711s (+74.7% 🔺)1.788s51.14x
▲ VercelExpress5.057s (+36.1% 🔺)6.144s (+31.4% 🔺)1.087s51.17x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.026s (-1.7%)4.439s (-8.8% 🟢)0.413s71.00x
💻 LocalNext.js (Turbopack)7.090s (-16.7% 🟢)7.518s (-16.7% 🟢)0.428s41.76x
💻 LocalNitro8.444s (+3.0%)9.025s (+2.9%)0.580s42.10x
💻 LocalExpress8.531s (+26.3% 🔺)9.017s (+28.6% 🔺)0.486s42.12x
🌐 MongoDBNext.js (Turbopack)10.310s (+4.7%)10.688s (+3.3%)0.378s32.56x
🐘 PostgresExpress47.451s (-10.1% 🟢)48.137s (-9.4% 🟢)0.686s111.79x
🐘 PostgresNitro50.241s (-0.8%)51.136s (~)0.895s112.48x
🐘 PostgresNext.js (Turbopack)58.909s59.121s0.212s114.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.881s (+18.6% 🔺)5.079s (+15.2% 🔺)1.197s61.00x
▲ VercelNext.js (Turbopack)4.436s (-38.1% 🟢)5.664s (-28.0% 🟢)1.228s61.14x
▲ VercelExpress5.385s (-30.6% 🟢)7.031s (-25.8% 🟢)1.646s51.39x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.143s (-5.8% 🟢)1.001s (~)0.011s (-6.6% 🟢)1.016s (~)0.873s101.00x
🌐 RedisNext.js (Turbopack)0.151s (+4.6%)1.000s (~)0.001s (~)1.007s (~)0.856s101.06x
💻 LocalExpress0.176s (+57.0% 🔺)1.002s (~)0.012s (+29.7% 🔺)1.017s (~)0.842s101.23x
💻 LocalNitro0.181s (+5.3% 🔺)1.003s (~)0.012s (+10.9% 🔺)1.018s (~)0.837s101.27x
🌐 MongoDBNext.js (Turbopack)0.496s (+3.0%)0.951s (-1.6%)0.002s (+7.1% 🔺)1.009s (~)0.513s103.47x
🐘 PostgresNext.js (Turbopack)0.747s0.793s0.001s1.011s0.263s105.23x
🐘 PostgresExpress2.148s (+88.0% 🔺)2.897s (+52.5% 🔺)0.001s (+8.3% 🔺)3.015s (+49.7% 🔺)0.867s1015.02x
🐘 PostgresNitro2.432s (+2.4%)2.611s (-2.2%)0.002s (+33.3% 🔺)3.015s (~)0.584s1017.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.916s (-15.6% 🟢)2.837s (-4.0%)0.844s (+378.3% 🔺)4.269s (+14.5% 🔺)2.353s101.00x
▲ VercelExpress2.351s (+19.3% 🔺)3.372s (+30.0% 🔺)0.577s (+6.3% 🔺)4.713s (+25.6% 🔺)2.362s101.23x
▲ VercelNext.js (Turbopack)2.548s (+36.5% 🔺)2.760s (+5.4% 🔺)0.612s (-1.5%)4.391s (+18.2% 🔺)1.843s101.33x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNext.js (Turbopack)6/12
▲ VercelNitro6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)🌐 Redis8/12
Nitro💻 Local9/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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious