Why the Temporal Bun SDK uses Bun and protobufs directly

The first Temporal-on-Bun prototype had an appealing story: keep Temporal Core, write a Zig bridge, expose it to Bun, and get a native worker without waiting for the official TypeScript SDK to support Bun.
It was a good experiment. It was also the wrong long-term shape.
The bridge forced useful questions. Where does a worker become trustworthy? What has to be deterministic? Which parts of the Node worker are incidental, and which parts are production machinery? But it put too much risk at the runtime boundary. The bridge owned polling, pending handles, build-id wiring, lifecycle state, callback cleanup, and FFI error paths. When polling hung, the failure did not feel like a small binding bug. It felt like owning another worker runtime.
The direction in proompteng/lab is now cleaner: run the SDK directly on Bun, generate Temporal protobuf types, and talk to Temporal through those contracts from TypeScript.
Less clever. Easier to debug.
What changed
The package is @proompteng/temporal-bun-sdk. It is not a wrapper around
@temporalio/worker, and it is not a Zig addon wearing a JavaScript coat. The
worker and client runtime live in TypeScript, run under Bun, and use generated
Temporal protobufs as the hard boundary with the server.
The repo makes that boundary explicit:
- Temporal API definitions live under
proto/temporal. buf.temporal.gen.yamlgenerates TypeScript intopackages/temporal-bun-sdk/src/proto.- The client imports generated service types such as
WorkflowServiceandOperatorService. - Workflow intents become Temporal command protobufs before the worker completes a task.
- Replay tooling reads real Temporal history events.
That is the important shift. The SDK does not need Bun to impersonate Node, and it does not need Zig to relay opaque bytes. It models Temporal messages directly enough that the hard parts are visible in TypeScript: commands, histories, timers, activities, retries, heartbeats, cancellation, sticky queues, build IDs, updates, search attributes, and determinism.
Why not keep pushing Zig?
Because the hard question was never whether Zig can call a C ABI. It can.
The hard question was whether this project should own a second runtime boundary while also proving Temporal correctness. The bridge had to mimic details the official Node path gets from years of use: poller behavior, activation lifecycle, shutdown, error propagation, build-id defaults, and log flushing.
A missed detail can look like a worker that is alive while no workflow actually moves. That is a terrible failure mode.
With Bun plus generated protos, the failures are closer to the protocol. If a command is wrong, replay should catch it. If cancellation is wrong, an integration test can exercise it. If a payload converter breaks history compatibility, the corpus should fail. Those are still real bugs, but they are plain bugs.
Why protobufs help
Temporal is protocol-heavy. A worker is credible when it can repeatedly do the same thing against history: produce the same commands, honor the same timers, route the same activity results, and fail loudly when code diverges from recorded reality.
Generated protobufs keep the SDK close to that contract. Workflow code records a
high-level intent like "schedule this activity" or "start this timer." Before
the worker completes the task, that intent becomes a Temporal command message.
Replay reads HistoryEvent messages and reconstructs the same determinism
state.
That is easier to reason about than a bridge. The SDK is asking TypeScript to describe Temporal messages accurately and asking Bun to run the application runtime well.
What Bun buys here
Bun helps most when it is the runtime, not an adapter target.
For this SDK, that means fast startup, direct TypeScript execution, a simple
local loop, straightforward Docker packaging, and one runtime for workflows,
activities, CLI helpers, and operational scripts. The package exports the worker
runtime, client, test helpers, replay commands, and a temporal-bun CLI from
the same Bun-first environment.
It does not make Temporal easy. Workflow code still needs deterministic guards. Timers, updates, queries, signals, child workflows, activity retries, and payload conversion still need careful behavior. The advantage is that those problems stay in the same language and runtime as the rest of the SDK.
The bar moved
The old article made the work sound like the main question was whether a native runtime could land on npm. That is not the bar anymore.
The better question is whether the package can produce enough evidence for a grounded choice. The lab package now has production evidence scripts, default-choice checks, package boundary checks that reject native bridge artifacts, replay fixtures, integration tests, worker load tests, and generated readiness artifacts.
That does not erase the tradeoff: this is still not the official Temporal SDK. If your requirement is official Temporal-maintained Core support, use the official Node SDK.
If your requirement is Bun-first workers, and you are willing to validate your own workflows with replay and load gates, the direct Bun/protobuf path is the cleaner bet.
The lesson
The Zig bridge was not wasted work. It made the production boundary visible.
The mistake would have been keeping the bridge as the story after the repo moved on. The current story is more practical: use Bun directly, generate the Temporal protocol, keep the worker runtime in TypeScript, and spend the hard work on determinism and operational proof instead of native glue.
That is less flashy. It is also what I would rather debug at 2 a.m.