Node SDK
Server-side ingest from any Node.js runtime, Vercel Functions, AWS Lambda, long-running services, scripts. Batches events with retry, jitter, and X-Idempotency-Key so transient network failures never double-count.
- Runtime: Node 18+.
- Module: ESM.
- Types: shipped in
dist/index.d.ts. - Zero runtime dependencies.
Use this package from trusted backend code. Browser code should use @getfluxly/browser with a publishable key.
Install
npm install @getfluxly/node
Quick start
import { initGFluxNode } from "@getfluxly/node";
const gflux = initGFluxNode({
token: process.env.GFLUX_SERVER_TOKEN,
apiHost: "https://api.getfluxly.com",
});
await gflux.track("subscription_started", {
userId: "user_42",
properties: { plan: "pro" },
});
await gflux.identify({
anonymousId: "anon_a8f3c2",
userId: "user_42",
traits: { email: "x@y.com", plan: "pro" },
});
await gflux.alias({
userId: "user_42",
anonymousId: "anon_a8f3c2",
});
await gflux.flush();
Init config
initGFluxNode(config) creates and returns a server-side client.
| Key | Default | Description |
|---|---|---|
token | required | API key. Use a gflux_secret_... key for backend code. |
apiHost | https://api.getfluxly.com | API origin. Validated with new URL() at startup. |
defaultContext | {} | Context object merged into every event before SDK audit fields are added. Per-call context is merged on top of this. |
includeRuntimeContext | false | Opt-in: attach node_version, platform, and arch to every event. Off by default to avoid sending a runtime fingerprint to SaaS backends. |
fetch | global fetch | Custom fetch implementation for tests or non-standard runtimes. |
logger | none | Logger invoked for retries (debug) and HTTP rejections, flush failures, and exhausted retries (warn). See Logging. |
flushAt | 20 | Number of queued events that triggers an automatic flush. Set to 1 for synchronous per-event delivery. |
flushIntervalMs | 5000 | Background flush interval in milliseconds. 0 disables the background timer. |
maxRetries | 2 | Retries after network errors, 408, 425, 429, and 5xx responses. |
timeoutMs | 5000 | Per-request timeout in milliseconds. 0 disables the timeout. |
maxQueueSize | 1000 | Hard cap on queued events. New events beyond this limit throw queue_overflow. |
track()
Send a backend event.
await gflux.track("invoice_paid", {
userId: "user_123",
properties: {
invoice_id: "inv_456",
total_usd: 99.99,
},
});
Identity rules:
userIdis the canonical key for a known user. It is an alias forexternalIdon the wire. If you pass bothuserIdandexternalIdin the same call, they must be identical strings.- Use
anonymousIdfor events where the user is not yet known. - You can pass both
userIdandanonymousIdwhen you already know they belong together. propertiesandcontextmust be plain objects.
identify()
Link anonymous activity to a known user. Both anonymousId and userId are required.
await gflux.identify({
anonymousId: "anon_abc",
userId: "user_123",
traits: {
email: "jane@example.com",
plan: "pro",
},
});
The event shape sent to the API mirrors the browser SDK: anonymous_id and external_id are both required, and traits travel under properties.traits. Earlier anonymous activity is stitched to the known user on the backend.
alias()
Call the server-only alias endpoint. Use a gflux_secret_... key. Publishable keys are rejected by the API.
await gflux.alias({
userId: "user_123",
anonymousId: "anon_current",
previousId: "anon_old",
});
At least one of anonymousId or previousId is required. Both are accepted when an anonymous session spanned multiple devices.
flush()
Flush all queued events immediately.
const result = await gflux.flush();
console.log(result.accepted, result.rejected);
flush() always returns a FlushResult with accepted and rejected counts. Batches are automatically split into chunks of 50 events because the API rejects larger payloads. Every chunk carries an X-Idempotency-Key header that persists across retries.
In short-lived runtimes (Lambda, Vercel Functions) call flush() before the handler returns, otherwise the batch may never ship.
shutdown()
Stop the background flush timer and drain the queue.
await gflux.shutdown();
Call this before a CLI, worker, or test process exits when batching is enabled. Queued events on the last interval are dropped if you skip this.
Batching
const gflux = initGFluxNode({
token: process.env.GFLUX_SERVER_TOKEN,
flushAt: 50,
flushIntervalMs: 10_000,
});
track() and identify() return null while events are queued. flush() and shutdown() always return a FlushResult.
Idempotency
Every batch and every alias request carries X-Idempotency-Key: <uuid>. The same key is reused across all retries of the same batch so the backend deduplicates them. The SDK retries 408, 425, 429, and 5xx responses, plus network failures, with exponential backoff and ±25% jitter.
Errors
Always check instanceof GFluxNodeError before inspecting fields.
import { GFluxNodeError } from "@getfluxly/node";
try {
await gflux.track("invoice_paid", { userId: "user_123" });
} catch (error) {
if (error instanceof GFluxNodeError) {
if (error.code === "queue_overflow") {
// Apply your own backoff; the batch is full.
await wait(error.retryAfterMs ?? 1000);
} else if (error.retryable) {
// Network/timeout/5xx: the SDK already exhausted maxRetries.
// Retry at your application boundary if the event is business-critical.
}
}
}
GFluxNodeError fields:
| Field | Type | Description |
|---|---|---|
code | string | Stable error code: validation_error, queue_overflow, network_error, request_timeout, invalid_response, http_4xx, client_closed, and others. |
status | number | undefined | HTTP status code when applicable. |
retryable | boolean | true for transient transport errors and 408/425/429/5xx responses. |
retryAfterMs | number | undefined | Suggested wait before re-enqueueing on queue_overflow. |
details | unknown | Backend error body when the server returned one. |
cause | unknown | Original error preserved when wrapping lower-level failures. |
Validation errors, 401, 403, and other permanent client errors are not retried.
Full stable code list at errors.
Event context
Every event includes minimal SDK audit fields:
{
"library": "gflux-node/0.2.0",
"runtime": "node"
}
These fields always win over any caller-supplied values so backend logs can reliably identify which package sent the event.
Pass includeRuntimeContext: true to also attach node_version, platform, and arch. Add your own context with defaultContext (merged into every event at init time) or with the per-call context field. Merge order: defaultContext first, then per-call context, then SDK audit fields on top.
Logging
const gflux = initGFluxNode({
token: process.env.GFLUX_SERVER_TOKEN,
logger: {
debug: (msg, ctx) => myLogger.debug(msg, ctx),
warn: (msg, ctx) => myLogger.warn(msg, ctx),
},
});
Hooks invoked:
debug: every transient retry (HTTP and network).warn: HTTP rejections, exhausted retries, scheduled flush failures, and requeues after retryable flush errors.