Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@subflag/openfeature-node-provider

OpenFeature Node.js provider for Subflag feature flags.

Installation

npm install @subflag/openfeature-node-provider @openfeature/server-sdk

or with pnpm:

pnpm add @subflag/openfeature-node-provider @openfeature/server-sdk

Quick Start

import { OpenFeature } from '@openfeature/server-sdk';
import { SubflagNodeProvider } from '@subflag/openfeature-node-provider';

// Initialize the provider
const provider = new SubflagNodeProvider({
  apiUrl: 'http://localhost:8080',
  apiKey: 'sdk-production-my-app-your-key-here',
});

// Set provider and wait for it to be ready
await OpenFeature.setProviderAndWait(provider);

// Get a client
const client = OpenFeature.getClient();

// Evaluate flags
const isEnabled = await client.getBooleanValue('new-feature', false);
const bannerText = await client.getStringValue('banner-text', 'Welcome!');
const maxItems = await client.getNumberValue('max-items', 10);
const config = await client.getObjectValue('ui-config', { theme: 'light' });

Configuration

SubflagProviderConfig

Option Type Required Description
apiUrl string Yes The Subflag API URL (e.g., "http://localhost:8080")
apiKey string Yes Your SDK API key (format: sdk-{env}-{random})
timeout number No Request timeout in milliseconds (default: 5000)
cache CacheConfig No Cache configuration (see Caching below)

Caching

By default, the provider makes an API call for every flag evaluation. For better performance, you can enable caching with a pluggable cache interface.

Using the Built-in InMemoryCache

import { OpenFeature } from '@openfeature/server-sdk';
import { SubflagNodeProvider, InMemoryCache } from '@subflag/openfeature-node-provider';

const provider = new SubflagNodeProvider({
  apiUrl: 'http://localhost:8080',
  apiKey: 'sdk-production-...',
  cache: {
    cache: new InMemoryCache(),
    ttlSeconds: 30, // Cache values for 30 seconds
  },
});

await OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient();

// First call hits the API
await client.getBooleanValue('my-flag', false);

// Subsequent calls within TTL use cached value
await client.getBooleanValue('my-flag', false); // No API call

Using Redis (or any custom cache)

Implement the SubflagCache interface to use Redis, Memcached, or any other cache:

import { SubflagNodeProvider, SubflagCache } from '@subflag/openfeature-node-provider';
import Redis from 'ioredis';

const redis = new Redis();

const redisCache: SubflagCache = {
  async get(key) {
    const data = await redis.get(key);
    return data ? JSON.parse(data) : undefined;
  },
  async set(key, value, ttlSeconds) {
    await redis.setex(key, ttlSeconds, JSON.stringify(value));
  },
  async delete(key) {
    await redis.del(key);
  },
  async clear() {
    // Optional: implement if needed
  },
};

const provider = new SubflagNodeProvider({
  apiUrl: 'http://localhost:8080',
  apiKey: 'sdk-production-...',
  cache: {
    cache: redisCache,
    ttlSeconds: 60,
  },
});

Cache Configuration Options

Option Type Required Description
cache SubflagCache Yes Cache implementation
ttlSeconds number No Time-to-live in seconds (default: 60)
keyGenerator function No Custom cache key generator

Custom Cache Keys

By default, cache keys are generated as subflag:{flagKey}:{contextHash}. You can customize this:

const provider = new SubflagNodeProvider({
  apiUrl: 'http://localhost:8080',
  apiKey: 'sdk-production-...',
  cache: {
    cache: new InMemoryCache(),
    ttlSeconds: 30,
    keyGenerator: (flagKey, context) => {
      return `myapp:flags:${flagKey}:${context?.targetingKey || 'anonymous'}`;
    },
  },
});

Context-Aware Caching

The cache automatically accounts for different evaluation contexts. Different users/contexts get separate cache entries:

// These use different cache entries
await client.getBooleanValue('premium-feature', false, { targetingKey: 'user-1' });
await client.getBooleanValue('premium-feature', false, { targetingKey: 'user-2' });

Prefetching Flags

For optimal performance, prefetch all flags in a single API call. This is especially useful in request handlers where you evaluate multiple flags:

import { OpenFeature } from '@openfeature/server-sdk';
import { SubflagNodeProvider, InMemoryCache } from '@subflag/openfeature-node-provider';

const provider = new SubflagNodeProvider({
  apiUrl: 'http://localhost:8080',
  apiKey: 'sdk-production-...',
  cache: {
    cache: new InMemoryCache(),
    ttlSeconds: 30,
  },
});

await OpenFeature.setProviderAndWait(provider);

// In your request handler:
app.get('/api/data', async (req, res) => {
  const context = { targetingKey: req.user.id };

  // Prefetch all flags for this user (1 API call)
  await provider.prefetchFlags(context);

  // All subsequent evaluations use cache (0 API calls)
  const client = OpenFeature.getClient();
  const showNewUI = await client.getBooleanValue('new-ui', false, context);
  const maxItems = await client.getNumberValue('max-items', 10, context);
  const theme = await client.getStringValue('theme', 'light', context);

  res.json({ showNewUI, maxItems, theme });
});

Note: prefetchFlags() requires caching to be enabled. It will throw an error if called without a cache configured.

Getting an API Key

See Team Management → API Keys in the docs for instructions on creating API keys.

Usage with Express.js

import express from 'express';
import { OpenFeature } from '@openfeature/server-sdk';
import { SubflagNodeProvider } from '@subflag/openfeature-node-provider';

const app = express();

// Initialize provider on startup
const provider = new SubflagNodeProvider({
  apiUrl: process.env.SUBFLAG_API_URL || 'http://localhost:8080',
  apiKey: process.env.SUBFLAG_API_KEY || '',
});

await OpenFeature.setProviderAndWait(provider);

// Get a client (can be reused across requests)
const featureClient = OpenFeature.getClient();

app.get('/api/data', async (req, res) => {
  // Check feature flag
  const usePagination = await featureClient.getBooleanValue('use-pagination', false);

  if (usePagination) {
    const pageSize = await featureClient.getNumberValue('page-size', 20);
    // Return paginated data
    res.json({ items: [], pageSize });
  } else {
    // Return all data
    res.json({ items: [] });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Environment Variables

Store your configuration in environment variables:

# .env
SUBFLAG_API_URL=http://localhost:8080
SUBFLAG_API_KEY=sdk-production-web-app-xQ7mK9nP2wR5tY8uI1oA3sD4

Then use a package like dotenv:

import 'dotenv/config';
import { OpenFeature } from '@openfeature/server-sdk';
import { SubflagNodeProvider } from '@subflag/openfeature-node-provider';

const provider = new SubflagNodeProvider({
  apiUrl: process.env.SUBFLAG_API_URL!,
  apiKey: process.env.SUBFLAG_API_KEY!,
});

Supported Flag Types

The provider supports all OpenFeature value types:

  • Boolean: getBooleanValue(flagKey, defaultValue)
  • String: getStringValue(flagKey, defaultValue)
  • Number: getNumberValue(flagKey, defaultValue)
  • Object: getObjectValue(flagKey, defaultValue)

Error Handling

The provider handles errors gracefully and returns default values when:

  • The flag doesn't exist (404)
  • The API key is invalid (401/403)
  • Network errors occur
  • The flag value type doesn't match the requested type
const client = OpenFeature.getClient();

// If 'missing-flag' doesn't exist, returns false
const value = await client.getBooleanValue('missing-flag', false);

// You can also get detailed evaluation information
const details = await client.getBooleanDetails('my-flag', false);
console.log(details.reason); // 'STATIC', 'DEFAULT', or 'ERROR'
console.log(details.variant); // Variant name (e.g., 'control', 'treatment')
console.log(details.errorCode); // Error code if reason is 'ERROR'

CommonJS Support

This package supports both ESM and CommonJS:

// ESM
import { SubflagNodeProvider } from '@subflag/openfeature-node-provider';

// CommonJS
const { SubflagNodeProvider } = require('@subflag/openfeature-node-provider');

Development

# Install dependencies
pnpm install

# Build the provider
pnpm build

# Run tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Type check
pnpm typecheck

License

MIT