SOASAP Node.js SDK
Local-first feature flag evaluation for Node.js 18+ — Express, Fastify, workers, and serverless handlers.
Overview
The SOASAP Node.js SDK synchronizes feature flag configuration from SOASAP Cloud in the background and evaluates flags from an in-memory snapshot. Flag reads are synchronous, O(1), and never perform network or disk I/O on the hot path.
The SDK provides:
- Local evaluation — dictionary lookups against an immutable snapshot
- Real-time synchronization — Server-Sent Events (SSE) delta updates
- Persistent cache — disk-backed snapshot for cold starts
- Offline operation — last known snapshot when the network or control plane is unavailable
- Explicit defaults — every getter accepts a code-defined fallback
- Non-blocking startup — optional
preload: truestarts sync without blocking module init
Runtime characteristics
- O(1) local evaluation — reads from an in-memory snapshot; no network requests during evaluation
- Real-time SSE synchronization — background worker applies validated payloads with atomic configuration updates
- Persistent cache — disk-backed snapshot for cold starts and restarts
- Offline operation — last known snapshot when the network or control plane is unavailable
- Explicit defaults — missing, mistyped, or unsynced keys return your code-defined fallback
- Non-blocking startup —
preload: truestarts synchronization without awaiting network I/O
Requirements
Supported runtimes:
- Node.js 18, 20, 22+
- ESM and CommonJS entry points
Supported application types:
- Express, Fastify, and other HTTP frameworks
- Background workers and job processors
- Serverless functions (with graceful
close()on shutdown) - Standalone scripts and CLI tools
Runs on Linux, Windows, and macOS. Requires outbound HTTPS to api.soasap.com for SSE synchronization.
Installation
npm:
npm install @soasap-com/node-sdk
pnpm:
pnpm add @soasap-com/node-sdk
yarn:
yarn add @soasap-com/node-sdk
package.json dependency:
{
"dependencies": {
"@soasap-com/node-sdk": "1.0.3"
}
}
Verify installation
npm list @soasap-com/node-sdk
Confirm @soasap-com/node-sdk appears in the project dependency tree before continuing.
Before you begin
You will need:
- ✓ A SOASAP account
- ✓ A project
- ✓ An environment
- ✓ An environment API key
If you have not completed these steps, start with Quick Start.
Installation flow
After your project and environment exist in the dashboard, install the npm package and call
createSoasapClient once at application startup. The API key binds the process to a single
environment. Synchronization runs in the background; request handlers evaluate flags locally.
Client initialization
Create one client per process and enable preload for production servers:
import { createSoasapClient } from '@soasap-com/node-sdk';
const apiKey = process.env.SOASAP_API_KEY;
if (!apiKey) {
throw new Error('SOASAP_API_KEY is required.');
}
const flags = createSoasapClient({
apiKey,
preload: true,
});
- API key — environment-scoped secret sent as
X-API-Keyon SSE requests; identifies which configuration to synchronize - One client per process — reuse the same
SoasapClientinstance across your application - Preload — with
preload: true, the SSE worker starts during construction without awaiting network I/O
Attach the client to your request context (middleware, AsyncLocalStorage, or module export) so handlers can read flags synchronously.
Configuration
Pass options to createSoasapClient. The SDK does not load .env files automatically — read secrets in your bootstrap code or use a loader such as dotenv.
Recommended configuration
Development
.env.localor.env.developmentwithdotenv- Never commit Development keys to shared branches if they grant access to sensitive environments
Production
- Environment variables
- Azure Key Vault
- AWS Secrets Manager
- Kubernetes Secrets
Never commit Production API keys to source control. Surface secrets as environment variables at deploy time.
Environment variable:
SOASAP_API_KEY=soasap...
const flags = createSoasapClient({
apiKey: process.env.SOASAP_API_KEY!,
preload: true,
});
Optional configuration:
import { SoasapErrorSource, createSoasapClient } from '@soasap-com/node-sdk';
const flags = createSoasapClient({
apiKey: process.env.SOASAP_API_KEY!,
baseUrl: 'https://api.soasap.com',
cacheDirectory: '/var/lib/myapp/soasap-cache',
preload: true,
onError: (ctx) => {
console.warn(`[SOASAP ${ctx.source}] transient=${ctx.isTransient}`, ctx.exception.message);
},
});
Evaluating flags
All getters read from the current in-memory snapshot. Wrong JSON types return the default you provide.
| Flag type | Method |
|---|---|
| Boolean | getBool |
| String | getString |
| Number | getNumber |
| JSON | getJson<T> |
Boolean
const enabled = flags.getBool('new-checkout', false);
getFlag is an alias for getBool.
String
const theme = flags.getString('checkout-theme', 'default');
Number
SOASAP number flags map to JavaScript number. Use for integers, limits, rates, and thresholds.
const maxItems = flags.getNumber('max-items', 10);
const discountRate = flags.getNumber('discount-rate', 0);
JSON
interface CheckoutSettings {
enabled: boolean;
timeoutSeconds: number;
}
const settings = flags.getJson<CheckoutSettings>('checkout-config', {
enabled: false,
timeoutSeconds: 30,
});
JSON keys from the dashboard are returned as stored (typically camelCase).
Map property names to match your TypeScript types. Non-object values or parse failures return the default; getters do not throw.
Explicit defaults
Always pass a default on every read:
flags.getBool('new-checkout', false);
The default is returned when:
- The flag key does not exist in the synchronized snapshot
- The flag was deleted or disabled in the dashboard for this environment
- The process starts before the first SSE payload arrives and no disk cache exists
- A restored disk cache is empty or invalid
- The flag value type does not match the getter (for example, a string flag read with
getBool) - Synchronization is temporarily delayed after deploy or reconnect
Explicit defaults are your production safety net. They define safe behavior when configuration is missing, stale, or not yet synchronized — without try/catch on the hot path.
false for feature gates and conservative values for limits.
Runtime architecture
- SSE delivers full or delta flag payloads to a background worker
- Valid payloads replace the in-memory snapshot atomically — readers never observe partial updates
- Evaluation reads a local reference to an immutable snapshot with no locks on the request path
- No HTTP requests and no blocking disk I/O during
getBool,getNumber,getString, orgetJson
Implementation details on the hot path:
- Snapshot replacement swaps an object reference atomically — concurrent readers keep the previous snapshot until the swap completes
- Each getter copies the current snapshot reference locally before lookup
- SSE parser enforces a 5 MB payload cap; invalid root payloads are ignored
Persistent cache
On construction, the client synchronously loads the last persisted JSON snapshot from disk before any network activity. SSE updates enqueue debounced disk writes (coalesced every ~2.5 seconds).
Default cache locations:
| Platform | Path |
|---|---|
| Windows | %LOCALAPPDATA%\soasap\cache\soasap_cache_{hash}.json |
| Linux / macOS | ~/.local/share/soasap/cache/soasap_cache_{hash}.json |
| Fallback | {cwd}/soasap/cache/soasap_cache_{hash}.json |
Override with cacheDirectory: '/custom/path'. Each API key maps to a separate cache file (hash derived from the key).
Deploy and restart — rolling deploys and pod restarts hydrate from disk immediately; SSE reconnects in the background. First deploy on a new machine without cache returns defaults until the first successful sync.
Offline operation
The SDK continues evaluating flags from the last known snapshot when:
- Internet connectivity is unavailable
- SOASAP Cloud returns errors or is unreachable
- The SSE connection drops (proxy timeout, firewall, transient network failure)
The background worker reconnects with exponential backoff. Reads are unaffected during disconnect — only future updates are delayed.
Stale configuration is preferable to unavailable configuration: the application keeps running with the last known values and your code defaults.
| Scenario | Behavior |
|---|---|
| API unavailable | Serves stale cached snapshot |
| SSE disconnected | Keeps last known snapshot; reconnects with backoff |
| First startup, no cache | Returns explicit defaults until first sync |
| Invalid SSE payload | Payload ignored; snapshot unchanged |
| Disk cache failure | In-memory mode continues; error reported via onError |
Preload and startup behavior
preload: true is recommended for production HTTP servers and long-running workers.
With preload:
- Client construction loads the disk cache synchronously
- The SSE worker starts immediately — no await during module initialization
- The server keeps booting while synchronization completes in the background
- First requests may read disk cache or explicit defaults until the first SSE payload arrives
Without preload (lazy mode):
const flags = createSoasapClient({
apiKey: process.env.SOASAP_API_KEY!,
});
The SSE worker starts on the first flag read. Until then, evaluation uses the disk cache (if present) or your explicit defaults. Prefer preload for predictable synchronization timing after deploy.
Graceful shutdown
Call close() when the process exits to cancel SSE, flush the disk cache (up to 5 seconds), and release resources:
process.on('SIGTERM', async () => {
await flags.close();
process.exit(0);
});
Required for clean deploys in container orchestration and serverless runtimes that reuse or freeze the process between invocations.
Best practices
- Create once — single
createSoasapClientcall at module or bootstrap scope. - Reuse the client — one instance per process and environment; export from a shared module.
- Do not share across worker threads — use one client on the main thread; pass flag values to
worker_threadsvia IPC if needed. - Store API keys securely — see Recommended configuration.
- Enable preload —
preload: truefor non-blocking background sync at startup. - Always pass explicit defaults — safe behavior when keys are missing or not yet synchronized.
- Do not create clients per request — avoids duplicate SSE connections and memory overhead.
- Call
close()on shutdown — flush cache and cancel the SSE worker cleanly. - Use environment-specific API keys — Development, Staging, and Production hold independent configuration.
Common issues
Flag always returns the default
- Verify the API key matches the intended environment
- Confirm the flag key exists and is enabled in that environment
- Check the flag type matches the getter (
getBoolfor boolean flags)
Dashboard changes do not appear
- Verify outbound HTTPS to
api.soasap.comand SSE is not blocked by a proxy or firewall - See SSE Disconnected
Wrong value returned
- Verify the flag value in the selected environment — Development and Production are independent
- Confirm the deployed process uses the API key for that environment
Multiple SDK instances
Symptoms
- Multiple SSE connections from the same process or deployment
- Increased memory usage
- Duplicate synchronization traffic
Resolution — call createSoasapClient once and reuse the same client throughout the application. Do not instantiate a new client inside request handlers or per-route factories.
Startup synchronization delays
Expected with preload: the process starts immediately; the first SSE payload may arrive seconds after boot. Until then, evaluation uses the disk cache or explicit defaults. This is by design — startup is never blocked on sync.
Complete example
The following example demonstrates the recommended production setup for Express applications, including configuration, client initialization, preload, middleware attachment, and runtime flag evaluation.
import express from 'express';
import { createSoasapClient } from '@soasap-com/node-sdk';
const apiKey = process.env.SOASAP_API_KEY;
if (!apiKey) {
throw new Error('Set SOASAP_API_KEY via environment variable.');
}
const flags = createSoasapClient({
apiKey,
preload: true,
});
const app = express();
app.use((req, _res, next) => {
req.soasap = flags;
next();
});
app.get('/checkout', (req, res) => {
const client = req.soasap;
if (!client.getBool('new-checkout', false)) {
res.json({ mode: 'legacy' });
return;
}
res.json({
mode: 'new',
maxItems: client.getNumber('max-items', 10),
theme: client.getString('checkout-theme', 'default'),
});
});
app.get('/health', (req, res) => {
if (req.soasap.getBool('maintenance-mode', false)) {
res.status(503).send('Maintenance');
return;
}
res.send('OK');
});
const server = app.listen(3000);
process.on('SIGTERM', async () => {
server.close();
await flags.close();
process.exit(0);
});
Production recommendations
- Store API keys in a secret manager; inject via environment variables at runtime
- Use separate API keys per environment — never share Production keys with Development builds
- Never commit Production keys to source control or embed them in container images unnecessarily
- Monitor outbound connectivity and SSE health; alert on prolonged disconnect in Production
- Keep explicit defaults in code for every flag read — defaults define behavior during outages
- Register
onErrorin Production to log background sync, disk, and parser failures without affecting reads - Call
await flags.close()on SIGTERM/SIGINT in containerized deployments
Next steps
- Feature Flags — flag types, defaults, and naming
- Architecture — local evaluation, SSE, cache, and offline operation
- API Keys — issuing, rotating, and securing environment keys
- Other SDKs — .NET, Python, React, and more
Deep dives: Initialization, Cache, Offline Behavior.