Skip to main content

Server SDK Integration Guide

This guide explains how to integrate the Pulse Server SDK and its Express and NestJS wrappers into your backend applications. It covers installation, configuration, and usage for each supported framework.

Overview

The Pulse Server SDK enables backend services to access Pulse settings, tracking, and context propagation. It is framework-agnostic and can be used directly or via wrappers for Express and NestJS.

  • @pulse/server: Core SDK for Node.js backends.
  • @pulse/express: Express.js middleware for request context.
  • @pulse/nest: NestJS module and middleware for seamless integration.

Installation

Install the required packages from your registry:

npm install @pulse/server @pulse/express @pulse/nest

Configuration

All integrations use a common configuration object. The most important option is app, which identifies your application. You can also specify settings for the tracker, settings, and other features.

Example configuration:

const config = {
app: "your-app-name",
settings: {
defaults: {
/* key-value pairs */
},
remoteSettingEnabled: true,
},
tracker: {
version: "your app version",
},
};

Usage

1. Direct Server SDK Usage

Use this approach for custom Node.js servers, request handlers outside Express/NestJS, or any code that owns its own request lifecycle.

import { createClient } from "@pulse/server";
import { pulse } from "@pulse/core";

const client = createClient(config);

// Wrap the per-request work in an async context — `pulse` from `@pulse/core`
// resolves to this request's context inside the callback (and any async branch
// from inside it).
await client.createAsyncContext(
{ userId: "user-123" /* ...other context fields, see PulseState */ },
async () => {
const value = await pulse.get("setting_name", "tag_name");
// ... handle the request, use settings / fire events ...
},
);

For long-running Node processes (CLIs, daemons, batch scripts) where there's no per-request boundary, see the dedicated CLI / Standalone Node Tracker guide. It documents the tracker surface (event, events, attribute, etc.), device-id persistence, and the wire format CLI events use.

For Next.js (App Router) — which has no Node middleware chain — see the Next.js Integration (Temporary Workaround) guide. It shows how to wrap route handlers with a withPulse helper until an official @pulse/next wrapper is available.


2. Express Integration

Use the Express middleware to automatically inject Pulse context into each request.

import { pulse } from "@pulse/core";
import { usePulse } from "@pulse/express";
import express from "express";

const host = process.env["HOST"] ?? "localhost";
const port = process.env["PORT"] ? Number(process.env["PORT"]) : 9000;

const app = express();

// Initialize Pulse middleware with configuration
app.use(
usePulse({
app: "your-app-name",
})
);

app.get("/", async (_req, res) => {
let settingValue = null;

try {
settingValue = await pulse.get("setting_name", "tag_name");
} catch (error) {
settingValue = { error: "Failed to fetch setting" };
}

res.send({
message: "Hello API!",
setting: settingValue,
});
});

Key Points:

  • The middleware is initialized with your app name.
  • In route handlers, you can access the Pulse context and fetch settings asynchronously.
  • Error handling is included for robustness.

3. NestJS Integration

@pulse/nest wraps the server SDK in a Nest module. Registering the module is the whole setup — from then on every service, controller, guard and interceptor reads the current request's Pulse context through the global pulse proxy, with no constructor injection and no request-scoped providers.

Requirements

@pulse/nest declares @nestjs/common ^11 and express ^5 as peer dependencies, so it targets NestJS 11 on the Express 5 platform adapter.

Step 1 — register the module

// app.module.ts
import { Module } from "@nestjs/common";
import { PulseModule } from "@pulse/nest";

@Module({
imports: [
PulseModule.forRoot({
app: "your-app-name", // the only required option
settings: {
host: process.env.PULSE_HOST, // defaults to https://optifyr.com
timeout: 1000,
offline: false,
cache: true,
contextCacheTTL: 600_000, // 10 minutes
},
tracker: {
appVersion: "1.2.3",
},
}),
CheckoutModule,
],
})
export class AppModule {}

PulseModule implements NestModule and registers PulseMiddleware from its own configure() hook, for every route including /. Do not apply the middleware yourself — a second consumer.apply(PulseMiddleware) would open a nested context for each request.

Step 2 — use pulse in your services

Import the pulse proxy from @pulse/core anywhere in your codebase. Feature modules do not need to import PulseModule.

// checkout.service.ts
import { Injectable } from "@nestjs/common";
import { pulse } from "@pulse/core";

@Injectable()
export class CheckoutService {
async getConfig() {
// Settings — note the argument order: (name, tag, defaultValue)
const enabled = await pulse.get<boolean>("new_checkout", "checkout", false);
const segments = await pulse.segments();
const experiments = await pulse.experiments();

// Tracking
pulse.event({ event: "checkout_viewed", data: { enabled } });
pulse.set({ tier: "pro" }); // session-level state attached to later events

return { enabled, segments, experiments };
}
}

The available surface is the same Pulse interface used everywhere else:

AreaMethods
Settingsget(name, tag, default), observe(name, tag, default), segments(), experiments()
Trackerevent(), events(), attribute(), attributes(), set(), state(), stateData(), getState(), flush()
SharedgetStateString()

What happens on each request

PulseMiddleware calls client.createAsyncRequestContext(...), which:

  1. Reads the Pulse state cookie and the request URL.
  2. Parses country (X-Client-Country, CF-IPCountry, Vercel-IP-Country, …), device id, traffic source (UTM / referrer / click id) and debug experiments into a request-scoped PulseContext.
  3. Builds a Pulse instance bound to that context and runs the rest of the chain inside a Node AsyncLocalStorage store.
  4. Writes the updated state back as a cookie before the response headers are sent.

pulse from @pulse/core resolves that store on every call, which is why it works from arbitrarily deep in the call graph — including across await boundaries — without threading a client through function arguments.

Work that runs outside a request

There is no request context in a cron job, a queue consumer or an OnModuleInit hook, so pulse.* has nothing to resolve: settings calls fall back to the default you passed after a ~6s timeout, and tracker calls such as pulse.event(...) never resolve at all.

For that work, inject PulseClient and open a context explicitly. PulseModule is not global, so the module doing this must import it:

// reports.module.ts
import { Module } from "@nestjs/common";
import { PulseModule } from "@pulse/nest";
import { ReportsService } from "./reports.service";

@Module({
imports: [PulseModule], // re-import to inject PulseClient; forRoot() stays in AppModule
providers: [ReportsService],
})
export class ReportsModule {}
// reports.service.ts
import { Injectable } from "@nestjs/common";
import { pulse } from "@pulse/core";
import { PulseClient } from "@pulse/server";

@Injectable()
export class ReportsService {
constructor(private readonly client: PulseClient) {}

async runNightlyJob(userId: string) {
await this.client.createAsyncContext({ userId }, async () => {
const batchSize = await pulse.get<number>(
"report_batch_size",
"reports",
100,
);
pulse.event({ event: "nightly_report_started", data: { batchSize } });
});
}
}

PulseModule also exports the PULSE_OPTIONS token if you need to read back the configuration it was registered with.

Notes

  • Ordering. Nest runs module middleware before guards, interceptors and route handlers, so all of those see the context. Middleware registered directly on the HTTP adapter in main.ts (app.use(...)) is installed ahead of it and runs without a Pulse context.
  • Route matching. The middleware is bound with the Express 5 wildcard '{*path}'. A bare '*' throws at bootstrap under path-to-regexp v8, and '*path' would silently skip the root route — so keep the module's own configure() rather than re-binding the middleware with a different pattern.
  • Errors. Failures while opening the context are forwarded to Nest's exception filters via next(error); once the chain has started, error handling is Nest's.

API Reference

createClient(config)

Creates a Pulse client instance with the given configuration.

Express: usePulse(config)

Returns Express middleware that injects Pulse context into each request.

NestJS: PulseModule.forRoot(config)

Registers the Pulse module, creates a single long-lived PulseClient, and applies PulseMiddleware to every route via the module's own configure() hook.

Exports:

  • PulseClient — inject it to open a context manually for non-HTTP work.
  • PULSE_OPTIONS — the configuration object the module was registered with.
  • PulseMiddleware — exported for testing; it is already applied for you.

Notes

  • Always initialize the SDK with your application name and relevant configuration.
  • For advanced scenarios, refer to the SDK source or contact the Pulse team.