Node
⭐ Recommended: Use the Reforge CLI to generate TypeScript definitions for type-safe access to your flags and configs:
npx @reforge-com/cli generate --targets node-ts
Install the Latest Version
- npm
- yarn
npm install @reforge-com/node
yarn add @reforge-com/node
TypeScript types are included with the package.
Initialize a Client
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
First, generate your types:
npx @reforge-com/cli generate --targets node-ts
Then use the generated typed class:
import { Reforge } from "@reforge-com/node";
import { ReforgeTypesafeNode } from "./generated/reforge-server";
const baseReforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY,
enableSSE: true,
enablePolling: true,
});
await baseReforge.init();
// Create the typed instance
const reforge = new ReforgeTypesafeNode(baseReforge);
// Now you get full type safety and autocomplete!
const myStringConfig = reforge.myStringConfig(); // string type inferred
const myFeatureFlag = reforge.coolFeature(); // boolean type inferred
const configWithContext = reforge.userSpecificSetting({
user: { key: "user-123" },
}); // Type-safe context
import { Reforge } from "@reforge-com/node";
const reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY,
enableSSE: true,
enablePolling: true,
});
await reforge.init();
import { Reforge } from "@reforge-com/node";
const reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY,
enableSSE: true,
enablePolling: true,
});
await reforge.init();
Next.js API Routes (Singleton Pattern)
For Next.js API routes and other serverless environments, use the singleton pattern to avoid re-initialization on every request:
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
// app/api/flags/route.ts (or pages/api/flags.ts)
import { Reforge, type Contexts } from "@reforge-com/node";
import { ReforgeTypesafeNode } from "../../../generated/reforge-server";
import { NextRequest, NextResponse } from "next/server";
let baseReforge: Reforge | null = null;
let reforge: ReforgeTypesafeNode | null = null;
async function getReforgeClient(): Promise<ReforgeTypesafeNode> {
if (!baseReforge || !reforge) {
baseReforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY!,
enableSSE: true,
enablePolling: true,
});
await baseReforge.init();
reforge = new ReforgeTypesafeNode(baseReforge);
}
return reforge;
}
export async function GET(request: NextRequest) {
const rf = await getReforgeClient();
// Get user context from request headers, cookies, etc.
const userId = request.headers.get("x-user-id");
const context: Contexts = {
user: { key: userId || "anonymous" },
};
// Use type-safe methods with context
const welcomeMessage = rf.welcomeMessage(context);
const isFeatureEnabled = rf.newFeature(context);
return NextResponse.json({
welcomeMessage,
isFeatureEnabled,
});
}
// app/api/flags/route.ts (or pages/api/flags.ts)
import { Reforge, type Contexts } from "@reforge-com/node";
import { NextRequest, NextResponse } from "next/server";
let reforge: Reforge | null = null;
async function getReforgeClient(): Promise<Reforge> {
if (!reforge) {
reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY!,
enableSSE: true,
enablePolling: true,
});
await reforge.init();
}
return reforge;
}
export async function GET(request: NextRequest) {
const rf = await getReforgeClient();
// Get user context from request
const userId = request.headers.get("x-user-id");
const context: Contexts = {
user: { key: userId || "anonymous" },
};
return rf.inContext(context, (contextRf) => {
const welcomeMessage = contextRf.get("welcomeMessage");
const isFeatureEnabled = contextRf.isFeatureEnabled("newFeature");
return NextResponse.json({
welcomeMessage,
isFeatureEnabled,
});
});
}
// app/api/flags/route.js (or pages/api/flags.js)
import { Reforge } from "@reforge-com/node";
let reforge = null;
async function getReforgeClient() {
if (!reforge) {
reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY,
enableSSE: true,
enablePolling: true,
});
await reforge.init();
}
return reforge;
}
export async function GET(request) {
const rf = await getReforgeClient();
// Get user context from request
const userId = request.headers.get("x-user-id");
const context = {
user: { key: userId || "anonymous" },
};
return rf.inContext(context, (contextRf) => {
const welcomeMessage = contextRf.get("welcomeMessage");
const isFeatureEnabled = contextRf.isFeatureEnabled("newFeature");
return Response.json({
welcomeMessage,
isFeatureEnabled,
});
});
}
This pattern prevents re-initialization on every API request, which is crucial for serverless environments where functions are frequently recycled. The client is initialized once per container lifecycle and reused across requests.
Make sure to use REFORGE_BACKEND_SDK_KEY (not NEXT_PUBLIC_) in API routes since they run server-side. Only client-side code needs the NEXT_PUBLIC_ prefix.
Feature Flags and Dynamic Config
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
With generated types, you get camelCase method names and full type safety:
// Instead of string keys, use type-safe methods:
if (reforge.newUserOnboarding()) {
// boolean type inferred
// Show new user flow
}
const maxRetries = reforge.apiRetryCount(); // number type inferred
const welcomeMessage = reforge.welcomeMessage(); // string type inferred
// Context is type-safe too
const userSpecificLimit = reforge.userDailyLimit({
user: { key: "user-123", plan: "pro" },
}); // Types enforced for context structure
After the init completes you can use:
// Feature flags
if (reforge.isFeatureEnabled("some.feature.name")) {
// returns boolean
// Feature is enabled
}
// Dynamic config
const configValue: string | undefined = reforge.get("some.config.name"); // type inferred from usage
const numberConfig: number | undefined = reforge.get("api.retry.count"); // number type
const booleanFlag: boolean | undefined = reforge.get("feature.enabled"); // boolean type
After the init completes you can use:
reforge.isFeatureEnabled('some.feature.name')returns true or falsereforge.get('some.config.name')returns a raw value
Context
Reforge Launch supports context for intelligent rule-based
evaluation of get and isFeatureEnabled based on the current request/device/user/etc.
Given
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
import type { Contexts } from "@reforge-com/node";
const context: Contexts = {
user: { key: "some-unique-identifier", country: "US" },
subscription: { key: "pro-sub", plan: "pro" },
};
With the generated typed class, context is passed directly to each method:
// Each generated method accepts context as an optional parameter
const configValue = reforge.someConfigName(context);
const isEnabled = reforge.someFeatureName(context);
// Or use inline context
const userSpecificValue = reforge.welcomeMessage({
user: { key: "user-123", language: "en" },
device: { mobile: true },
}); // All type-safe!
You can also use the base reforge instance for inContext blocks:
baseReforge.inContext(context, (rf) => {
// Use the typed instance inside the context
const typedRf = new ReforgeTypesafeNode(rf);
console.log(typedRf.someConfigName());
console.log(typedRf.someFeatureName());
});
import type { Contexts } from "@reforge-com/node";
const context: Contexts = {
user: { key: "some-unique-identifier", country: "US" }, // user context
subscription: { key: "pro-sub", plan: "pro" }, // subscription context
};
You can pass this in to each call
reforge.get('some.config.name', context, defaultValue)// context-aware configreforge.isFeatureEnabled('some.feature.name', context, false)// context-aware feature flag
Or you can set the context in a block (perhaps surrounding evaluation of a web request)
reforge.inContext(context, (rf) => {
const optionalJustInTimeContext = { device: { mobile: true } }; // additional context
console.log(
rf.get("some.config.name", optionalJustInTimeContext, defaultValue),
); // merged context
console.log(
rf.isFeatureEnabled("some.feature.name", optionalJustInTimeContext, false),
); // boolean result
});
const context = {
user: { key: "some-unique-identifier", country: "US" },
subscription: { key: "pro-sub", plan: "pro" },
};
You can pass this in to each call
reforge.get('some.config.name', context, defaultValue)reforge.isFeatureEnabled('some.feature.name', context, false)
Or you can set the context in a block (perhaps surrounding evaluation of a web request)
reforge.inContext(context, (rf) => {
const optionalJustInTimeContext = { ... }
console.log(rf.get("some.config.name", optionalJustInTimeContext, defaultValue))
console.log(rf.isEnabled("some.config.name", optionalJustInTimeContext, false))
})
Dynamic Log Levels
The Reforge SDK provides integration with popular Node.js logging frameworks, enabling you to dynamically manage log levels across your application in real-time without restarting.
Features
- Centrally manage log levels - Control logging across your entire application from the Reforge dashboard
- Real-time updates - Change log levels without restarting your application
- Context-aware logging - Different log levels for different loggers based on runtime context
- Framework support - Works with Pino and Winston
Pino Integration
Pino is a fast JSON logger for Node.js. Install it as a peer dependency:
- npm
- yarn
npm install pino
yarn add pino
Create a Pino logger with dynamic level support:
import { Reforge, createPinoLogger } from "@reforge-com/node";
const reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY!,
enableSSE: true,
});
await reforge.init();
// Create logger with dynamic log levels from Reforge
const logger = createPinoLogger(reforge, "myapp.services");
// Log levels are controlled dynamically by Reforge
logger.debug("Debug message");
logger.info("Info message");
logger.error("Error message");
Alternatively, add dynamic level control to an existing Pino logger:
import pino from "pino";
import { createPinoHook } from "@reforge-com/node";
const logger = pino({
mixin: createPinoHook(reforge, "myapp.services"),
});
Winston Integration
Winston is a versatile logging library. Install it as a peer dependency:
- npm
- yarn
npm install winston
yarn add winston
Create a Winston logger with dynamic level support:
import { Reforge, createWinstonLogger } from "@reforge-com/node";
const reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY!,
enableSSE: true,
});
await reforge.init();
// Create logger with dynamic log levels from Reforge
const logger = createWinstonLogger(reforge, "myapp.services");
// Log levels are controlled dynamically by Reforge
logger.debug("Debug message");
logger.info("Info message");
logger.error("Error message");
Alternatively, add dynamic level control to an existing Winston logger:
import winston from "winston";
import { createWinstonFormat } from "@reforge-com/node";
const logger = winston.createLogger({
format: winston.format.combine(
createWinstonFormat(reforge, "myapp.services"),
winston.format.json()
),
transports: [new winston.transports.Console()],
});
Configuration
Create a LOG_LEVEL_V2 config in your Reforge dashboard with key log-levels.default:
# Default to INFO for all loggers
default: INFO
# Set specific packages to DEBUG
rules:
- criteria:
reforge-sdk-logging.logger-path:
starts-with: "myapp.services"
value: DEBUG
# Only log errors in noisy third-party library
- criteria:
reforge-sdk-logging.logger-path:
starts-with: "somelib"
value: ERROR
You can customize the config key name using the loggerKey option. This is useful if you have multiple applications sharing the same Reforge project and want to isolate log level configuration per application:
const reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY!,
loggerKey: "myapp.log.levels",
});
The SDK automatically includes lang: "node" in the evaluation context, which you can use in your rules to create Node.js-specific log level configurations:
# Different log levels for Node.js vs other languages
rules:
- criteria:
reforge-sdk-logging.lang: node
reforge-sdk-logging.logger-path:
starts-with: "myapp"
value: DEBUG
- criteria:
reforge-sdk-logging.lang: java
reforge-sdk-logging.logger-path:
starts-with: "com.example"
value: INFO
Targeted Log Levels
You can use rules and segmentation to change your log levels based on the current user/request/device context. This allows you to increase log verbosity for specific users, environments, or conditions without affecting your entire application.
The log level evaluation has access to all context that is available during evaluation, not just the reforge-sdk-logging context. This means you can create rules combining:
- SDK logging context (
reforge-sdk-logging.*) - Logger name and language - Global context - Application name, environment, availability zone, etc.
- Dynamic context - User, team, device, request information from context-scoped evaluations
For example, you can create rules like:
# Enable DEBUG logs only for specific application in staging
rules:
- criteria:
application.key: "myapp"
application.environment: "staging"
reforge-sdk-logging.logger-path:
starts-with: "myapp"
value: DEBUG
# Enable DEBUG logs for a specific user across all applications
- criteria:
user.email: "developer@example.com"
value: DEBUG
# Lower verbosity in production
- criteria:
application.environment: "production"
value: WARN
This allows you to increase log verbosity for specific users, specific applications, particular environments, or any combination of conditions without affecting your entire system.
Direct Log Level API
You can also check log levels programmatically using the getLogLevel method:
const logLevel = reforge.getLogLevel("myapp.services.auth");
console.log(`Current log level: ${logLevel}`); // e.g., "INFO", "DEBUG", "WARN"
// Use it to conditionally log expensive operations
if (reforge.getLogLevel("myapp.analytics") === "DEBUG") {
// Only compute and log expensive analytics in debug mode
logger.debug("Detailed analytics:", computeExpensiveAnalytics());
}
How It Works
Once configured, the integration automatically filters all logging calls:
- Works with all loggers that use the integration
- Filters happen before log messages are formatted (performance benefit)
- Checks configuration on every log call for real-time updates
- No modification of your existing logging configuration needed
Mustache Templating
Reforge supports Mustache templating for dynamic string configurations, allowing you to create personalized messages, URLs, and other dynamic content.
Prerequisites
Install Mustache as a peer dependency:
- npm
- yarn
npm install mustache
yarn add mustache
Example Configuration
In your Reforge dashboard, create a string configuration with Mustache variables:
- Configuration Key:
welcome.message - Configuration Type:
json - Value:
{
"message": "Hello {{userName}}! Welcome to {{appName}}. You have {{creditsCount}} credit(s) remaining.",
"cta": "Buy More Credits"
} - Zod Schema (optional, for validation):
z.object({
message: z.string(),
cta: z.string(),
});
Usage Examples
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
The CLI generates type-safe template functions:
import {ReforgeTypesafeNode} from './generated/reforge-server';
// Get the configured object
const welcomeMessageObject = reforge.welcomeMessage();
// Template functions are generated automatically
// Returns: "Hello Alice! Welcome to MyApp. You have 150 credits remaining."
const welcomeText = welcomeMessageObject.message({
// Type-safe parameters
userName: 'Alice', // string type
appName: 'MyApp', // number type
creditsCount: 150, // string type
});
});
// Returns: Buy More Credits
const welcomeCta = welcomeMessageObject.cta;
For non-generated usage, you'll need to handle templating manually:
import Mustache from "mustache";
// Get the configured object
const welcomeMessageObject = reforge.get("welcome.message");
// Returns: "Hello Alice! Welcome to MyApp. You have 150 credits remaining."
const welcomeText = Mustache.render(welcomeMessageObject.message, {
userName: "Alice", // string type
appName: "MyApp", // number type
creditsCount: 150, // string type
});
// Returns: Buy More Credits
const welcomeCta = welcomeMessageObject.cta;
For raw javascript usage, handle templating manually:
const Mustache = require("mustache");
// Get the configured object
const welcomeMessageObject = reforge.get("welcome.message");
// Returns: "Hello Alice! Welcome to MyApp. You have 150 credits remaining."
const welcomeText = Mustache.render(welcomeMessageObject.message, {
userName: "Alice",
appName: "MyApp",
creditsCount: 150,
});
// Returns: Buy More Credits
const welcomeCta = welcomeMessageObject.cta;
Context-Aware Templating
Combine templating with Reforge context for personalized experiences:
const userContext = {
user: { key: "user-123", plan: "premium" },
device: { mobile: false },
};
// Template adapts based on context
const personalizedContentObject = reforge.dynamicContent(userContext);
const personalizedMessage = personalizedContentObject.message({
name: "Alice",
isPremium: true,
deviceType: "desktop",
});
const someOtherField = personalizedContentObject.someOtherProperty;
Config Change Listeners
Monitor configuration changes in real-time using config change listeners:
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
// Add a listener for config changes
const unsubscribe = reforge.addConfigChangeListener((changes) => {
console.log("Configuration changed:", changes);
// React to specific config changes
changes.forEach((change) => {
if (change.key === "database.connection.pool.size") {
// Reconfigure database connection pool
updateDatabasePool(change.newValue);
}
});
});
// Remove the listener when done
unsubscribe();
import type { ConfigChangeCallback } from "@reforge-com/node";
const changeHandler: ConfigChangeCallback = (changes) => {
console.log("Configs changed:", changes);
};
const unsubscribe = reforge.addConfigChangeListener(changeHandler);
// Clean up when done
unsubscribe();
const unsubscribe = reforge.addConfigChangeListener((changes) => {
console.log("Configs changed:", changes);
});
// Clean up when done
unsubscribe();
Config change listeners are useful for:
- Updating application state when configs change
- Triggering cache invalidation
- Logging configuration changes for debugging
- Implementing reactive configuration management
Advanced Methods
Raw Config Access
Access raw configuration metadata and structure:
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
// Get raw config with full metadata
const rawConfig = reforge.raw("api.retry.count");
if (rawConfig) {
console.log("Config type:", rawConfig.configType);
console.log("Value type:", rawConfig.valueType);
console.log("Rules:", rawConfig.rows);
}
const rawConfig = reforge.raw("api.retry.count");
console.log(rawConfig); // Full config object with metadata
const rawConfig = reforge.raw("api.retry.count");
console.log(rawConfig); // Full config object with metadata
Available Keys
Get all available configuration keys:
const allKeys = reforge.keys();
console.log(allKeys); // ["config.one", "config.two", "feature.flag", ...]
Default Context
Access the default context for the current environment:
const defaultCtx = reforge.defaultContext();
console.log("Default context:", defaultCtx);
Runtime Configuration Override
Set configuration values at runtime (useful for testing or dynamic overrides):
// Override a config value at runtime
reforge.set("feature.beta", { bool: true });
// The override persists until the next config update or restart
const isEnabled = reforge.isFeatureEnabled("feature.beta"); // true
Connection Management
Manual Updates
Force immediate configuration updates:
// Force an update now
await reforge.updateNow();
// Update only if configs are stale (older than 5 minutes)
reforge.updateIfStalerThan(5 * 60 * 1000); // 5 minutes in ms
Connection Control
// Stop polling for updates (SSE continues if enabled)
reforge.stopPolling();
// Properly close all connections and clean up resources
reforge.close(); // Stops SSE, clears timeouts, disables telemetry
Important: Always call reforge.close() when shutting down your application to ensure proper
cleanup of connections and prevent memory leaks.
Telemetry
Reforge automatically collects telemetry data to help you understand how your configurations are being used. The telemetry system includes several components:
Telemetry Components
// Access telemetry components
console.log("Evaluation summaries:", reforge.telemetry?.evaluationSummaries);
console.log("Context shapes:", reforge.telemetry?.contextShapes);
console.log("Example contexts:", reforge.telemetry?.exampleContexts);
console.log("Known loggers:", reforge.telemetry?.knownLoggers);
Configuration
Control telemetry collection via constructor options:
const reforge = new Reforge({
sdkKey: process.env.REFORGE_BACKEND_SDK_KEY!,
// Control evaluation summaries (default: true)
collectEvaluationSummaries: true,
// Control logger usage tracking (default: true)
collectLoggerCounts: true,
// Control context data collection (default: "periodicExample")
contextUploadMode: "SHAPE_ONLY", // or "NONE" or "periodicExample"
});
Context Upload Modes:
"periodicExample"- Sends both context structure and example values"SHAPE_ONLY"- Sends only context keys and types, no values"NONE"- Disables context collection entirely
Reference
Option Definitions
| Name | Description | Default |
|---|---|---|
| collectEvaluationSummaries | Send counts of config/flag evaluation results back to Reforge Launch to view in web app | true |
| collectLoggerCounts | Send counts of logger usage back to Reforge Launch to power log-levels configuration screen | true |
| contextUploadMode | Upload either context "shapes" (the names and data types your app uses in reforge contexts) or periodically send full example contexts | "periodicExample" |
| defaultLevel | Level to be used as the min-verbosity for a loggerPath if no value is configured in Reforge Launch | "warn" |
| enableSSE | Whether or not we should listen for live changes from Reforge Launch | true |
| enablePolling | Whether or not we should poll for changes from Reforge Launch | true |
| loggerKey | The config key to use for dynamic log level configuration (defaults to "log-levels.default") | "log-levels.default" |