Skip to main content

Next.js Integration (Temporary Workaround)

Temporary workaround

Pulse does not yet ship a dedicated Next.js wrapper (like @pulse/express or @pulse/nest). The approach below wires the core @pulse/server SDK into the Next.js App Router by hand. It works today, but treat it as an interim solution until an official @pulse/next package lands.

A complete, runnable reference implementation lives in the pulse-web repo: dev/pulse-next-js-dev.

Why a workaround is needed

Next.js has no real server middleware chain. Its middleware.ts runs on the Edge runtime as a lightweight proxy — not a Node.js request handler — so we cannot inject Pulse the way we do for Express or NestJS.

Two consequences follow:

  1. We can't use middleware to open the request context. Instead, each App Router route handler is wrapped explicitly so it runs inside a Pulse request-scoped context.
  2. Route handlers using Pulse must run on the Node.js runtime. The Pulse Server SDK relies on Node's async_hooks / AsyncLocalStorage, which the Edge runtime does not provide.

Step 1 — Create a shared Pulse helper

Create a single PulseClient at module-load time and expose a withPulse wrapper that opens a request-scoped context via client.createAsyncRequestContext. That context is what pulse.* (from @pulse/core) resolves against for the lifetime of the request.

The client is cached on globalThis so Next.js hot reloads (which re-evaluate modules on every edit in dev) don't rebuild transports and caches on each reload.

// src/lib/pulse.ts
import { createClient, PartialServerConfigurations, PulseClient } from '@pulse/server';
import type { NextRequest } from 'next/server';

const PULSE_APP = 'picsart.com';

const PULSE_CONFIG: PartialServerConfigurations = {
app: PULSE_APP,
settings: {
host: 'https://optifyr.com',
timeout: 30_000,
offline: false,
cache: true,
contextCacheTTL: 600_000,
},
};

// Reuse a single client across requests and across dev hot reloads.
const globalForPulse = globalThis as typeof globalThis & {
θpulseClient?: PulseClient;
};

export const pulseClient: PulseClient = (globalForPulse.θpulseClient ??=
createClient(PULSE_CONFIG));

type RouteHandler<Args extends unknown[]> = (
request: NextRequest,
...args: Args
) => Response | Promise<Response>;

/**
* Wraps an App Router route handler so it runs inside a Pulse request context.
* The state cookie is written back onto the returned `Response` automatically
* by `createAsyncRequestContext`.
*/
export function withPulse<Args extends unknown[]>(
handler: RouteHandler<Args>,
): (request: NextRequest, ...args: Args) => Promise<Response> {
return (request, ...args) =>
pulseClient.createAsyncRequestContext(
// The client config's `app` is not auto-injected into the request
// context, so we seed it here (same as the Express/Nest wrappers).
PULSE_CONFIG,
() => handler(request, ...args),
request,
);
}

Step 2 — Wrap your route handlers

Wrap each App Router route handler with withPulse, and pin the route to the Node.js runtime. Inside the handler, resolve settings with pulse.get(...) from @pulse/core, exactly as in the other server integrations.

// src/app/api/checkout-modal/route.ts
import { pulse } from '@pulse/core';
import { NextResponse } from 'next/server';
import { withPulse } from '../../../lib/pulse';

// Pulse's server SDK relies on Node's async_hooks, so this route must run on
// the Node.js runtime (not Edge).
export const runtime = 'nodejs';

export const GET = withPulse(async () => {
const create_widgets_dashboard = await pulse.get(
'create_widgets_dashboard',
'create_widgets_dashboard',
{ defaultValue: true },
);

return NextResponse.json({ create_widgets_dashboard });
});

Key points

  • No middleware. Wrap every route handler that needs Pulse with withPulse; there is no global registration step.
  • export const runtime = 'nodejs' is required on any route that touches Pulse — the Edge runtime lacks async_hooks.
  • Seed app in the context. The client config's app is not auto-injected into the request context, so withPulse passes PULSE_CONFIG again — mirroring what the Express and NestJS wrappers do internally.
  • The state cookie is written back automatically onto the Response returned from createAsyncRequestContext.
  • Cache the client on globalThis to survive dev hot reloads.

Reference implementation

The full example — Nx project setup, next.config.mjs, ambient CSS type declarations, and the route handlers above — is maintained in pulse-web:

Until an official @pulse/next wrapper is available, use this directory as the canonical starting point.