Core Concepts
Understand the architecture, event system, and design patterns of the Estuary Web SDK.
Architecture
The EstuaryClient is the main entry point. It composes several internal modules:
- SocketManager handles the WebSocket connection, authentication, reconnection logic, and event routing.
- VoiceManager captures microphone audio and streams it to the server. Two implementations exist: WebSocket-based and LiveKit-based.
- MemoryClient provides access to the memory REST API for querying facts, knowledge graphs, and conversation history.
REST calls (client.memory, client.getCharacter()) go to the versioned /api/v1 routes and are authenticated with your API key. The SDK also sends an X-Estuary-Client: estuary-ts-sdk/<version> header on every REST request, so Estuary can see which client versions are in use (see Client identification). The header is never sent on the Socket.IO connection.
Event-Driven Design
The SDK uses a typed EventEmitter pattern. Every event has a defined callback signature through the EstuaryEventMap interface, giving you full TypeScript autocompletion and type safety.
// Events are fully typed
client.on('botResponse', (response) => {
// response is typed as BotResponse
console.log(response.text);
});
client.on('connected', (session) => {
// session is typed as SessionInfo
console.log(session.sessionId);
});
You can subscribe with on(), unsubscribe with off(), and use once() for one-time listeners:
// Listen until removed
const handler = (response: BotResponse) => { /* ... */ };
client.on('botResponse', handler);
client.off('botResponse', handler);
// Listen once
client.once('connected', (session) => {
console.log('First connection:', session.sessionId);
});
Connection Lifecycle
The client tracks its connection through five states:
import { ConnectionState } from '@estuary-ai/sdk';
client.on('connectionStateChanged', (state) => {
switch (state) {
case ConnectionState.Connecting:
showSpinner();
break;
case ConnectionState.Connected:
hideSpinner();
break;
case ConnectionState.Reconnecting:
showReconnectingBanner();
break;
case ConnectionState.Error:
showErrorMessage();
break;
}
});
When autoReconnect is enabled (the default), the client automatically attempts to reconnect on disconnect with linear backoff (delay × attemptNumber, e.g. 2s, 4s, 6s, 8s, 10s with the default 2000ms base delay), up to maxReconnectAttempts (default: 5).
Error Handling
All SDK errors are instances of EstuaryError, which extends the standard Error class with a typed code property:
import { EstuaryError, ErrorCode } from '@estuary-ai/sdk';
client.on('error', (err) => {
if (err instanceof EstuaryError) {
switch (err.code) {
case ErrorCode.AUTH_FAILED:
console.error('Invalid API key');
break;
case ErrorCode.QUOTA_EXCEEDED:
console.error('Usage limit reached');
break;
case ErrorCode.VOICE_NOT_SUPPORTED:
console.error('Voice not available in this environment');
break;
default:
console.error('Error:', err.message);
}
}
});
Methods like connect(), startVoice(), and toggleMute() throw EstuaryError synchronously or reject with one. Always wrap connect() in a try/catch:
try {
await client.connect();
} catch (err) {
if (err instanceof EstuaryError && err.code === ErrorCode.AUTH_FAILED) {
// Handle invalid credentials
}
}
REST errors
REST calls (client.memory.*, client.getCharacter(), EstuaryClient.openShare()) reject with ErrorCode.REST_ERROR. The HTTP status is in err.message, and the parsed response body is in err.details.
One case is worth handling explicitly. If the API key lacks the scope a route needs, the server answers 403 and names the missing scope in the body:
{
"detail": {
"error": "insufficient_scope",
"message": "This API key lacks the 'memories:write' scope",
"requiredScope": "memories:write"
}
}
That body arrives on err.details:
try {
await client.memory.deleteAll(true);
} catch (err) {
if (err instanceof EstuaryError && err.code === ErrorCode.REST_ERROR) {
const detail = (err.details as { detail?: { error?: string; requiredScope?: string } })?.detail;
if (detail?.error === 'insufficient_scope') {
console.error(`This API key is missing the ${detail.requiredScope} scope`);
}
}
}
Generate a key with the scopes your integration needs from the Estuary dashboard. Keys created before scopes existed hold every scope, so older integrations are unaffected. See API Key Scopes for the full list.
Configuration
The EstuaryConfig object controls all client behavior. Only serverUrl, apiKey, characterId, and playerId are required:
const client = new EstuaryClient({
// Required
serverUrl: 'https://api.estuary-ai.com',
apiKey: 'est_your_api_key',
characterId: 'your-character-uuid',
playerId: 'user-123',
// Optional
voiceTransport: 'auto', // 'websocket' | 'livekit' | 'auto'
audioSampleRate: 24000, // Hz (preferred TTS playback rate)
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelayMs: 2000, // ms
debug: false,
realtimeMemory: false, // Enable real-time memory extraction events
suppressMicDuringPlayback: false, // Mute mic while bot audio plays (software AEC)
autoInterruptOnSpeech: true, // Interrupt bot audio when user starts speaking
});
See the Configuration Reference for details on every option.
Next Steps
- Text Chat -- Send messages and handle streaming responses
- Voice (WebSocket) -- Add voice input and output
- API Reference -- Full reference for all classes, events, and types