The Togul SSE stream pushes flag and rule change events to connected clients in real time. You can use it to keep a local flag cache up to date without polling the management API.
Endpoint
The response uses Content-Type: text/event-stream and stays open until you close the connection.
Authentication
Two authentication methods are supported:
| Method | Scope | Events received |
|---|
| Bearer token (JWT) | Organization-scoped | All flag and rule events across the organization |
API key with sdk or stream scope | Environment-scoped | Events for the API key’s specific environment |
Pass the API key using the X-API-Key header.
Event types
| Type | Trigger |
|---|
flag.created | A new flag was created |
flag.updated | A flag’s name, state, or default value changed |
flag.deleted | A flag was deleted |
rule.created | A new rule was added to a flag |
rule.updated | A rule was modified |
rule.deleted | A rule was removed |
Event payload
Each event is a JSON object sent as the data field of an SSE message:
data: {"type":"flag.updated","flag_key":"new-dashboard","environment_id":"env-uuid-here","version":0,"timestamp":1710432000}
| Field | Type | Description |
|---|
type | string | The event type (e.g. flag.updated) |
flag_key | string | The key of the affected flag |
environment_id | string | UUID of the affected environment |
version | integer | Monotonically increasing version counter |
timestamp | integer | Unix timestamp of the event |
Heartbeats
The server sends a comment line every 30 seconds to keep the connection alive through proxies and load balancers:
SSE clients ignore comment lines by default, so no special handling is needed.
Propagation
Events are published through an outbox-driven propagation loop. This means secondary regions consume changes asynchronously — there may be a small delay between a change in the primary region and its appearance in a secondary region’s stream.
Connecting with JavaScript
The browser’s built-in EventSource API handles SSE connections, including automatic reconnection.
const stream = new EventSource('http://localhost:8080/api/v1/stream', {
headers: {
'X-API-Key': 'tgl_sdk_abc123xyz...'
}
});
stream.onmessage = (event) => {
const payload = JSON.parse(event.data);
console.log('Received event:', payload.type, payload.flag_key);
};
stream.onerror = (err) => {
console.error('Stream error, will reconnect automatically:', err);
};
EventSource in browsers does not support custom headers. For browser use, pass the API key as a query parameter if your server supports it, or use a server-side proxy that injects the header.
Connecting with Node.js
Use the eventsource npm package or the native fetch API with a readable stream.
import EventSource from 'eventsource';
const stream = new EventSource('http://localhost:8080/api/v1/stream', {
headers: {
'X-API-Key': 'tgl_sdk_abc123xyz...'
}
});
stream.addEventListener('message', (event) => {
const payload = JSON.parse(event.data);
handleFlagEvent(payload);
});
stream.addEventListener('error', () => {
// EventSource reconnects automatically after an error.
// Log or alert if needed.
console.warn('SSE connection lost, reconnecting...');
});
Handling reconnections
EventSource reconnects automatically after a dropped connection. The browser uses the Last-Event-ID header to resume from the last received event if the server supports it.
For more control, implement manual reconnection with exponential backoff:
let retryDelay = 1000;
function connect() {
const stream = new EventSource('http://localhost:8080/api/v1/stream', {
headers: { 'X-API-Key': 'tgl_sdk_abc123xyz...' }
});
stream.onmessage = (event) => {
retryDelay = 1000; // Reset on success
handleFlagEvent(JSON.parse(event.data));
};
stream.onerror = () => {
stream.close();
setTimeout(() => {
retryDelay = Math.min(retryDelay * 2, 30000);
connect();
}, retryDelay);
};
}
connect();
Practical example: invalidating a local flag cache
A common pattern is to hold an in-memory cache of flag values and invalidate entries when the stream signals a change.
const flagCache = new Map();
async function getFlag(flagKey, context) {
if (flagCache.has(flagKey)) {
return flagCache.get(flagKey);
}
const response = await fetch('http://localhost:8080/api/v1/evaluate', {
method: 'POST',
headers: {
'X-API-Key': 'tgl_sdk_abc123xyz...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
flag_key: flagKey,
environment_key: 'production',
context
})
});
const result = await response.json();
flagCache.set(flagKey, result.value);
return result.value;
}
// Subscribe to the stream and invalidate cache on flag changes
const stream = new EventSource('http://localhost:8080/api/v1/stream', {
headers: { 'X-API-Key': 'tgl_sdk_abc123xyz...' }
});
stream.onmessage = (event) => {
const payload = JSON.parse(event.data);
if (
payload.type === 'flag.updated' ||
payload.type === 'flag.deleted' ||
payload.type === 'rule.updated' ||
payload.type === 'rule.deleted' ||
payload.type === 'rule.created'
) {
// Invalidate the cached value so the next call re-evaluates
flagCache.delete(payload.flag_key);
console.log(`Cache invalidated for flag: ${payload.flag_key}`);
}
};