npm package: @flagpool/sdk
Description: Official Flagpool SDK for TypeScript/JavaScript - feature flags with local evaluation, deterministic rollouts, and encrypted target lists
Author: Flagpool
Latest version: 0.4.4

# Flagpool SDK for TypeScript / JavaScript

Official TypeScript/JavaScript SDK for [Flagpool](https://flagpool.io) - the modern feature flag platform for teams who want control without complexity.

[![npm version](https://img.shields.io/npm/v/@flagpool/sdk.svg)](https://www.npmjs.com/package/@flagpool/sdk)
[![npm downloads](https://img.shields.io/npm/dm/@flagpool/sdk.svg)](https://www.npmjs.com/package/@flagpool/sdk)

## Installation

```bash
npm install @flagpool/sdk
```

## Quick Start

```typescript
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: 'alice@example.com',
    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

```typescript
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: 'user@example.com',
    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

```typescript
if (client.isEnabled('feature-flag')) {
  // Feature is enabled for this user
}
```

#### String Flags

Perfect for A/B tests and feature variants:

```typescript
const variant = client.getValue('button-color');
// Returns: 'blue' | 'green' | 'orange'
```

#### Number Flags

Great for limits, thresholds, and configurations:

```typescript
const limit = client.getValue('rate-limit');
// Returns: 100 | 1000 | 10000
```

#### JSON Flags

For complex configurations:

```typescript
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:

```typescript
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')); // 100
```

## Real-time Updates

Flags automatically refresh in the background:

```typescript
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:

```typescript
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

```typescript
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:**

```javascript
// 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:**

```typescript
// 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:

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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:

```typescript
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:

1. **Your environment has target list encryption enabled** (the default)
2. **You haven't set up a crypto adapter**

**Solutions:**

1. **Set up a crypto adapter** (recommended for production):
   ```typescript
   setCryptoAdapter(yourCryptoAdapter);
   ```

2. **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:

📚 **[flagpool.io/docs](https://flagpool.io/docs)**

## Support

- 📖 [Documentation](https://flagpool.io/docs)
- 🐛 [Report Issues](https://github.com/flagpool/flagpool-sdk/issues)
- ✉️ [Email Support](mailto:support@flagpool.io)

## License

MIT © [Flagpool](https://flagpool.io)