Skip to main content

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

  1. Client connects to /api/sdk/v1/replication/stream
  2. Server sends all current configs related to the SDK key
  3. Connection stays open
  4. Server pushes changes as they happen
  5. 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
// ...
}