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
withPulsehelper until an official@pulse/nextwrapper 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.
@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:
| Area | Methods |
|---|---|
| Settings | get(name, tag, default), observe(name, tag, default), segments(), experiments() |
| Tracker | event(), events(), attribute(), attributes(), set(), state(), stateData(), getState(), flush() |
| Shared | getStateString() |
What happens on each request
PulseMiddleware calls client.createAsyncRequestContext(...), which:
- Reads the Pulse state cookie and the request URL.
- Parses country (
X-Client-Country,CF-IPCountry,Vercel-IP-Country, …), device id, traffic source (UTM / referrer / click id) and debug experiments into a request-scopedPulseContext. - Builds a
Pulseinstance bound to that context and runs the rest of the chain inside a NodeAsyncLocalStoragestore. - 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 ownconfigure()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.