@flagpool/sdk
v0.4.4
Published
Official Flagpool SDK for TypeScript/JavaScript - feature flags with local evaluation, deterministic rollouts, and encrypted target lists
Maintainers
Readme
Flagpool SDK for TypeScript / JavaScript
Official TypeScript/JavaScript SDK for Flagpool - the modern feature flag platform for teams who want control without complexity.
Installation
npm install @flagpool/sdkQuick Start
import { FlagpoolClient } from '@flagpool/sdk';
const client = new FlagpoolClient({
projectId: 'your-project-uuid', // Get from Flagpool dashboard
apiKey: 'fp_production_xxx', // Environment-specific API key
decryptionKey: 'fp_dec_xxx', // For target list decryption & CDN URL
context: {
userId: 'user-123',
email: '[email protected]',
plan: 'pro',
country: 'US'
}
});
await client.init();
// Boolean flag
if (client.isEnabled('new-dashboard')) {
showNewDashboard();
}
// String flag (A/B test)
const buttonColor = client.getValue('cta-button-color');
// 'blue' | 'green' | 'orange'
// Number flag
const maxUpload = client.getValue('max-upload-size-mb');
// 10 | 100 | 1000
// JSON flag
const config = client.getValue('checkout-config');
// { showCoupons: true, maxItems: 50, paymentMethods: [...] }
// Clean up when done
client.close();Features
- ✅ Local evaluation - No server roundtrip per flag check
- ✅ Deterministic rollouts - Same user always gets same variation
- ✅ Multiple flag types - Boolean, string, number, JSON
- ✅ Advanced targeting - 8 operators including target lists
- ✅ Real-time updates - Automatic polling for flag changes
- ✅ Offline support - Works with cached flags when offline
- ✅ Zero dependencies - Lightweight, no external dependencies
- ✅ Analytics - Track flag evaluations (paid plans only)
Configuration
const client = new FlagpoolClient({
// Required
projectId: 'your-project-uuid', // Project ID from dashboard
apiKey: 'fp_production_xxx', // Environment-specific API key
decryptionKey: 'fp_dec_xxx', // For CDN URL hash & target list decryption
// Optional
context: { // User context for targeting
userId: 'user-123',
email: '[email protected]',
plan: 'pro',
// Add any attributes for targeting
},
pollingInterval: 30000, // Auto-refresh interval (ms), default: 30000
urlOverride: undefined, // Complete URL override (for self-hosted/testing)
analytics: { // Analytics (opt-in, paid plans only)
enabled: true, // Default: false
},
});API Reference
Methods
| Method | Description |
|--------|-------------|
| init() | Initialize client and fetch flags (required before evaluation) |
| isEnabled(key) | Check if a boolean flag is enabled |
| getValue(key) | Get flag value (any type) |
| getVariation(key) | Alias for getValue |
| updateContext(ctx) | Update user context and re-evaluate flags |
| onChange(callback) | Subscribe to flag value changes |
| getAnalyticsState() | Get current analytics state (for debugging) |
| getAllFlagsWithState() | Get all flags with evaluation status |
| flushAnalytics() | Manually flush analytics buffer |
| close() | Clean up resources (stop polling, flush analytics) |
Flag Types
Boolean Flags
if (client.isEnabled('feature-flag')) {
// Feature is enabled for this user
}String Flags
Perfect for A/B tests and feature variants:
const variant = client.getValue('button-color');
// Returns: 'blue' | 'green' | 'orange'Number Flags
Great for limits, thresholds, and configurations:
const limit = client.getValue('rate-limit');
// Returns: 100 | 1000 | 10000JSON Flags
For complex configurations:
const config = client.getValue('checkout-config');
// Returns: { showCoupons: true, maxItems: 50, ... }Targeting Rules
Flagpool supports powerful targeting with 8 operators:
| Operator | Description | Example |
|----------|-------------|---------|
| eq | Equals | plan == "enterprise" |
| neq | Not equals | plan != "free" |
| in | In list | country in ["US", "CA"] |
| nin | Not in list | country not in ["CN", "RU"] |
| contains | String contains | email contains "@company.com" |
| startsWith | String starts with | userId startsWith "admin-" |
| inTargetList | In target list | userId in beta-testers |
| notInTargetList | Not in target list | userId not in blocked-users |
Dynamic Context Updates
Update user context on the fly - flags re-evaluate automatically:
const client = new FlagpoolClient({
projectId: 'your-project-uuid',
apiKey: 'fp_prod_xxx',
decryptionKey: 'fp_dec_xxx',
context: { userId: 'user-1', plan: 'free' }
});
await client.init();
// User on free plan
console.log(client.getValue('max-upload-size-mb')); // 10
// User upgrades to pro
client.updateContext({ plan: 'pro' });
// Instantly gets pro limits
console.log(client.getValue('max-upload-size-mb')); // 100Real-time Updates
Flags automatically refresh in the background:
const client = new FlagpoolClient({
projectId: 'your-project-uuid',
apiKey: 'fp_prod_xxx',
decryptionKey: 'fp_dec_xxx',
context: { userId: 'user-1' },
pollingInterval: 30000 // Refresh every 30 seconds
});
await client.init();
// Listen for flag changes
client.onChange((flagKey, newValue) => {
console.log(`Flag ${flagKey} changed to:`, newValue);
// React to changes (e.g., update UI)
if (flagKey === 'maintenance-mode' && newValue === true) {
showMaintenanceBanner();
}
});Offline Support
The SDK caches flags locally. If the network is unavailable, it uses cached values:
const client = new FlagpoolClient({
projectId: 'your-project-uuid',
apiKey: 'fp_prod_xxx',
decryptionKey: 'fp_dec_xxx',
context: { userId: 'user-1' }
});
// Works even if network fails (uses cache from last successful fetch)
await client.init();
// Always returns a value (from cache if offline)
const feature = client.isEnabled('my-feature');Analytics (Paid Plans Only)
Track flag evaluation counts to understand usage patterns. Analytics is opt-in and disabled by default.
Enabling Analytics
const client = new FlagpoolClient({
projectId: 'your-project-uuid',
apiKey: 'fp_prod_xxx',
decryptionKey: 'fp_dec_xxx',
analytics: {
enabled: true,
}
});Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enabled | boolean | false | Enable/disable analytics |
| flushInterval | number | 60000 | Flush interval in ms (minimum: 30000) |
| flushThreshold | number | 100 | Flush after N evaluations |
| sampleRate | number | 1.0 | Sample rate (0.0 - 1.0) |
| syncFlushOnShutdown | boolean | false | Sync flush on process exit |
How It Works
- Evaluation counts are batched in memory
- Batches are sent asynchronously (fire-and-forget)
- Analytics never blocks flag evaluation
- Data is aggregated daily in your Flagpool dashboard
- Note: Data is silently discarded for free plan projects
Debugging Analytics
Browser Console:
// Inspect analytics state
console.log(__FLAGPOOL__.state.analytics);
// { enabled: true, buffer: { 'my-flag': 5 }, bufferSize: 5, ... }
// Get all flags with evaluation status
console.log(__FLAGPOOL__.state.flags);
// { 'my-flag': { value: true, evaluated: true }, ... }
// Force flush
__FLAGPOOL__.flushAnalytics();Programmatic API:
// Get analytics state
const state = client.getAnalyticsState();
console.log(state?.buffer); // { 'my-flag': 5 }
console.log(state?.bufferSize); // 5
// Get all flags with evaluation status
const flags = client.getAllFlagsWithState();
// { 'my-flag': { value: true, evaluated: true }, 'other-flag': { value: false, evaluated: false } }Serverless Environments
For AWS Lambda, Vercel Functions, or similar short-lived environments:
const client = new FlagpoolClient({
projectId: 'your-project-uuid',
apiKey: 'fp_prod_xxx',
decryptionKey: 'fp_dec_xxx',
analytics: {
enabled: true,
flushThreshold: 10, // Lower threshold
syncFlushOnShutdown: true, // Flush before exit
}
});Privacy
- Only flag keys and evaluation counts are tracked
- No user data or context is sent
- Data is retained for 3 months
Framework Examples
React
import { useEffect, useState } from 'react';
import { FlagpoolClient } from '@flagpool/sdk';
const client = new FlagpoolClient({
projectId: 'your-project-uuid',
apiKey: 'fp_prod_xxx',
decryptionKey: 'fp_dec_xxx',
});
export function useFeatureFlag(key: string) {
const [value, setValue] = useState<any>(null);
useEffect(() => {
client.init().then(() => {
setValue(client.getValue(key));
});
const unsubscribe = client.onChange((changedKey, newValue) => {
if (changedKey === key) setValue(newValue);
});
return () => unsubscribe?.();
}, [key]);
return value;
}
// Usage
function MyComponent() {
const showNewFeature = useFeatureFlag('new-feature');
if (showNewFeature) {
return <NewFeature />;
}
return <OldFeature />;
}Next.js
// lib/flagpool.ts
import { FlagpoolClient } from '@flagpool/sdk';
let client: FlagpoolClient | null = null;
export async function getFlags(userId: string) {
if (!client) {
client = new FlagpoolClient({
projectId: process.env.FLAGPOOL_PROJECT_ID!,
apiKey: process.env.FLAGPOOL_API_KEY!,
decryptionKey: process.env.FLAGPOOL_DECRYPTION_KEY!,
context: { userId }
});
await client.init();
}
return {
newDashboard: client.isEnabled('new-dashboard'),
buttonColor: client.getValue('button-color'),
};
}Node.js / Express
import express from 'express';
import { FlagpoolClient } from '@flagpool/sdk';
const app = express();
const flagpool = new FlagpoolClient({
projectId: process.env.FLAGPOOL_PROJECT_ID!,
apiKey: process.env.FLAGPOOL_API_KEY!,
decryptionKey: process.env.FLAGPOOL_DECRYPTION_KEY!,
});
// Initialize on startup
await flagpool.init();
app.get('/api/data', (req, res) => {
// Update context per-request
flagpool.updateContext({ userId: req.user.id });
if (flagpool.isEnabled('new-api-response')) {
return res.json({ version: 'v2', data: newData });
}
return res.json({ version: 'v1', data: legacyData });
});Target List Encryption
By default, target lists (used for inTargetList and notInTargetList operators) are encrypted in SDK exports to protect sensitive user data like emails and user IDs.
Encrypted Target Lists (Default)
If your environment has target list encryption enabled, you must provide a crypto adapter:
import { FlagpoolClient, setCryptoAdapter, CryptoAdapter } from '@flagpool/sdk';
// Create a Web Crypto adapter for browsers
const webCryptoAdapter: CryptoAdapter = {
async decrypt(ciphertext, key, iv, tag) {
const keyBase64 = key.startsWith('tlk_') ? key.slice(4) : key;
const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
const ciphertextBytes = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
const tagBytes = Uint8Array.from(atob(tag), c => c.charCodeAt(0));
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']
);
const combined = new Uint8Array(ciphertextBytes.length + tagBytes.length);
combined.set(ciphertextBytes, 0);
combined.set(tagBytes, ciphertextBytes.length);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: ivBytes }, cryptoKey, combined
);
return new TextDecoder().decode(decrypted);
}
};
// Set up crypto adapter BEFORE creating client
setCryptoAdapter(webCryptoAdapter);
const client = new FlagpoolClient({
projectId: 'your-project-id',
apiKey: 'your-api-key',
decryptionKey: 'tlk_xxx...', // From Flagpool dashboard
context: { userId: 'user-123' }
});Error: "Encrypted target lists received but no crypto adapter configured"
If you see this error, it means:
- Your environment has target list encryption enabled (the default)
- You haven't set up a crypto adapter
Solutions:
Set up a crypto adapter (recommended for production):
setCryptoAdapter(yourCryptoAdapter);Disable encryption (for development/testing):
- Go to Flagpool Dashboard → Settings → Environments
- Toggle off "Target List Encryption" for your environment
- This will export target lists in plaintext (values will be visible)
Plaintext Target Lists
If you disable target list encryption in the dashboard, no crypto adapter is needed. The SDK will use target lists directly without decryption.
⚠️ Security Note: Disabling encryption exposes target list values (emails, user IDs, etc.) in plaintext in the SDK exports. Only disable for development environments or non-sensitive data.
Documentation
For complete documentation, guides, and best practices, visit:
Support
License
MIT © Flagpool
