JavaScript SDK Guide
Configuration, testing, and best practices for the JavaScript/TypeScript SDK.
Type safety
Define your config types for full TypeScript support:
interface Configs {
'feature-dark-mode': boolean
'api-rate-limit': number
'allowed-regions': string[]
'pricing': {
free: { requests: number }
premium: { requests: number }
}
}
const replane = new Replane<Configs>()
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
// TypeScript knows the types
const darkMode = replane.get('feature-dark-mode') // boolean
const regions = replane.get('allowed-regions') // string[]
const pricing = replane.get('pricing') // { free: {...}, premium: {...} }
// Type error - 'invalid-config' doesn't exist
const invalid = replane.get('invalid-config')
Context and overrides
Context is used to evaluate override rules. Pass it at the client level or per request.
Client-level context
Applied to all get() calls:
const replane = new Replane<Configs>({
context: {
env: 'production',
region: 'us-east'
}
})
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
// Uses client context
const value = replane.get('config-name')
Per-evaluation context
Merged with client context:
const value = replane.get('feature-flag', {
context: {
userId: user.id,
plan: user.plan
}
})
Context properties
Common context properties:
{
userId: 'user-123', // User identifier
plan: 'premium', // Subscription tier
region: 'us-east', // Geographic region
deviceType: 'mobile', // Device type
appVersion: '2.1.0', // App version
env: 'production' // Environment
rateLimit: 100, // Number
isAdmin: true, // Boolean
}
Default values
Provide default values to use before connecting or if the config is not found:
const replane = new Replane<Configs>({
defaults: {
'feature-flag': false,
'rate-limit': 100,
'timeout-ms': 5000
}
})
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
The client starts with default values and updates when connection is established.
Realtime updates
The SDK maintains a persistent SSE connection for realtime updates.
How it works
- Client connects to
/api/sdk/v1/replication/stream - Server sends all current configs related to the SDK key
- Connection stays open
- Server pushes changes as they happen
get()always returns the latest value
Subscribing to changes
// Subscribe to specific config
const unsubFeature = replane.subscribe('feature-flag', (config) => {
// React to change
updateUI(config.value)
})
// Unsubscribe when done
unsubFeature()
React integration
import { useEffect, useState } from 'react';
function useConfig<K extends keyof Configs>(name: K) {
const [value, setValue] = useState(() => replane.get(name));
useEffect(() => {
return replane.subscribe(name, (config) => {
setValue(config.value as Configs[K]);
});
}, [name]);
return value;
}
// Usage
function App() {
const darkMode = useConfig('feature-dark-mode');
return <div className={darkMode ? 'dark' : 'light'}>...</div>;
}
Error handling
Connection errors
const replane = new Replane<Configs>()
try {
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
} catch (error) {
if (error instanceof ReplaneError) {
console.error('Replane error:', error.code, error.message)
}
// Use fallback configuration from defaults
}
Config not found
// Option 1: Catch the error
try {
const value = replane.get('missing-config')
} catch (error) {
if (error instanceof ReplaneError && error.code === 'not_found') {
console.error('Config not found')
}
}
// Option 2: Use a default value (recommended)
const value = replane.get('missing-config', { default: 'fallback' })
// Returns 'fallback' if config doesn't exist, no error thrown
Testing
In-memory client
Use defaults without calling connect() for tests:
import { Replane } from '@replanejs/sdk'
const replane = new Replane<Configs>({
defaults: {
'feature-flag': true,
'rate-limit': 100,
'pricing': { free: { requests: 100 }, premium: { requests: 10000 } }
}
})
// No connect() call - works purely from defaults
expect(replane.get('feature-flag')).toBe(true)
Custom fetch
Mock the fetch function:
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ configs: [...] })
});
const replane = new Replane<Configs>()
await replane.connect({
sdkKey: 'test-key',
baseUrl: 'https://test.com',
fetchFn: mockFetch
});
Multiple projects
Each SDK key is tied to one project and environment. For multiple projects, create separate clients:
const projectA = new Replane<ProjectAConfigs>()
await projectA.connect({
sdkKey: process.env.PROJECT_A_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
const projectB = new Replane<ProjectBConfigs>()
await projectB.connect({
sdkKey: process.env.PROJECT_B_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
Best practices
Initialize once
Create the client once at application startup:
// config.ts
export const replane = new Replane<Configs>()
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
// app.ts
import { replane } from './config'
const value = replane.get('feature-flag')
Clean up on shutdown
process.on('SIGTERM', () => {
replane.disconnect()
})
process.on('SIGINT', () => {
replane.disconnect()
})
Use defaults for resilience
const replane = new Replane<Configs>({
defaults: {
// Sensible defaults if Replane is unavailable
'feature-flag': false,
'rate-limit': 100
}
})
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY,
baseUrl: 'https://replane.example.com'
})
Type your configs
Always define config types for safety and autocomplete:
interface Configs {
'feature-new-ui': boolean
'max-upload-size': number
// ...
}