Next.js instrumentation.ts meets initServerRuntime
Next.js ships a lifecycle hook called instrumentation.ts. We just exposed initServerRuntime() from tako.sh/internal. Snap them together and Tako’s durable workflows, cross-process signals, and channel publishes start working inside your Next.js routes and server actions. Five lines, one file.
Why Next.js needs a boot hook
Most Tako apps are a single fetch handler — the SDK’s runtime entrypoint imports your module and the workflow/channel plumbing is installed in the same process that handles requests. Next.js standalone is structured differently. Our Next.js adapter wraps next start and spawns it as a child process, then proxies requests to it. The Tako SDK’s boot hook fires in the parent, but your app/ and pages/ handlers execute in the child.
Without a boot step on the child side, calling defineWorkflow(...).enqueue(payload), signal(event, payload), or channel.publish(...) from inside a route throws TakoError("TAKO_UNAVAILABLE", "Workflow runtime not installed. ..."). Everything else — typed secrets, env, logger, tako.gen.ts imports — already works, because those are static imports that don’t depend on process-level state.
What initServerRuntime() does
It’s the same boot step Tako’s plain runtime entrypoint performs, now callable directly. One call per process, idempotent, safe to import in any Node context:
| Step | Effect |
|---|---|
| Install the channel socket publisher | channel.publish(...) can send to subscribers on other instances |
| Register the workflow runtime | defineWorkflow(...).enqueue() and signal() reach the Tako workflow engine |
| Assert the parent-provided socket env is sane | Fail loud if the child was spawned without Tako’s internal env vars |
It lives on tako.sh/internal because it’s plumbing — app code never calls it directly.
Wire it up
Drop this file at the root of your Next.js project, next to next.config.ts:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { initServerRuntime } = await import("tako.sh/internal");
initServerRuntime();
}
}
That’s it. Next.js calls register() once per server process, before any route runs. The NEXT_RUNTIME === "nodejs" guard skips the Edge runtime, where tako.sh/internal doesn’t belong — it reads from a Node fd pipe and speaks over a unix-domain socket.
Now your server code does the obvious thing:
// app/api/checkout/route.ts
import fulfillOrder from "@/workflows/fulfill-order";
import orderEvents from "@/channels/order-events";
export async function POST(req: Request) {
const order = await req.json();
await fulfillOrder.enqueue({ orderId: order.id });
await orderEvents({ orderId: order.id }).publish({ type: "placed", data: order });
return Response.json({ ok: true });
}
Enqueue a multi-step durable workflow, publish to a live channel, send a signal() to a waiting run — all from a standard Next.js route or server action, with typed payloads from the same defineWorkflow/defineChannel calls you’d use in a plain Tako app.
The bigger picture
Tako’s goal for Next.js is for it to feel like any other fetch handler: withTako() in your config, a generated tako.gen.ts for typed runtime state and secrets, and now instrumentation.ts for the piece the child-process model made awkward. No monkeypatching, no framework globals, no TakoServer wrapper object — just Next.js’s own lifecycle hook calling one SDK function.
Same backend primitives, same deploy flow, same SDK — tako deploy still rolls a Next.js app exactly like any other Node/Bun app. The CLI reference and framework guides cover the rest.