Server-Sent Events (SSE)
MDDB provides a real-time event stream for document changes via Server-Sent Events (SSE).
Available since v2.9.4. Auth enforcement and per-IP rate limiting since v2.9.4.
Quick Start
curl -N http://localhost:11023/v1/events curl -N http://localhost:11023/v1/events?collection=blog curl -N -H "Authorization: Bearer YOUR_JWT_TOKEN" \ http://localhost:11023/v1/events?collection=blog curl -N -H "X-API-Key: mddb_live_xxx" \ http://localhost:11023/v1/events
Events
SSE broadcasts three event types when documents change:
| Event | Trigger |
|---|---|
doc.added | New document created |
doc.updated | Existing document updated |
doc.deleted | Document deleted |
Event Format
event: doc.added
data: {"event":"doc.added","collection":"blog","key":"post1","lang":"en","timestamp":1711324800,"readOnly":true} event: doc.updated
data: {"event":"doc.updated","collection":"blog","key":"post1","lang":"en","timestamp":1711324860,"readOnly":false} event: doc.deleted
data: {"event":"doc.deleted","collection":"blog","key":"old-post","lang":"en","timestamp":1711324900,"readOnly":false}
Connected Event
On connection, the server sends a connected event with session info:
event: connected
data: {"status":"connected","collection":"blog","mode":"readwrite","user":"admin"}
| Field | Description |
|---|---|
collection | Filtered collection (empty = all) |
mode | "read" or "readwrite" β indicates if user can write |
user | Authenticated username (empty if no auth) |
Keep-Alive
The server sends a comment every 30 seconds to prevent proxy timeouts:
: keepalive 1711324830
Authentication & Permissions
Without Auth (default)
When authentication is not configured on the server, SSE is open to everyone in read-only mode. All events are delivered with readOnly: true.
With Auth Enabled
| Scenario | Result |
|---|---|
| No token | 401 Unauthorized |
| Token, no PermRead on collection | Events for that collection are skipped |
| Token + PermRead | Events delivered, readOnly: true |
| Token + PermWrite | Events delivered, readOnly: false |
| Admin user | All events, readOnly: false |
The readOnly field tells the client whether it can write to the collection where the event occurred. This is useful for UI β e.g., showing/hiding edit buttons.
Rate Limiting
SSE enforces per-IP connection limits to prevent resource exhaustion:
| Setting | Default | Description |
|---|---|---|
MDDB_SSE_MAX_CLIENTS | 1000 | Max total concurrent SSE connections |
MDDB_SSE_MAX_PER_IP | 5 | Max concurrent SSE connections per IP |
When the limit is exceeded, the server returns 429 Too Many Requests.
IP detection supports proxy headers: X-Forwarded-For, X-Real-IP, and RemoteAddr.
Configuration
| Env Var | Default | Description |
|---|---|---|
MDDB_SSE_ENABLED | true | Enable/disable SSE endpoint |
MDDB_SSE_MAX_CLIENTS | 1000 | Max total connections |
MDDB_SSE_MAX_PER_IP | 5 | Max connections per IP |
To disable SSE entirely:
MDDB_SSE_ENABLED=false ./mddbd
Endpoints
| Port | Path | Description |
|---|---|---|
| HTTP (11023) | GET /v1/events | Main SSE endpoint |
| MCP (9000) | GET /events | SSE on MCP port |
Both endpoints support the ?collection=NAME query parameter for filtering.
Client Examples
JavaScript (Browser)
const source = new EventSource('http://localhost:11023/v1/events?collection=blog'); source.addEventListener('connected', (e) => { const data = JSON.parse(e.data); console.log('Connected:', data.mode, data.user);
}); source.addEventListener('doc.added', (e) => { const data = JSON.parse(e.data); console.log('New doc:', data.collection, data.key); if (!data.readOnly) { // Show edit button }
}); source.addEventListener('doc.updated', (e) => { const data = JSON.parse(e.data); console.log('Updated:', data.collection, data.key);
}); source.addEventListener('doc.deleted', (e) => { const data = JSON.parse(e.data); console.log('Deleted:', data.collection, data.key);
}); source.onerror = (e) => { console.error('SSE error, will auto-reconnect');
};
JavaScript with Auth (EventSource doesn't support headers)
Use fetch with ReadableStream for authenticated SSE:
async function subscribeSSE(token, collection = '') { const url = `http://localhost:11023/v1/events?collection=${collection}`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); // Parse SSE format: "event: ...\ndata: ...\n\n" for (const block of text.split('\n\n')) { const lines = block.split('\n'); const eventLine = lines.find(l => l.startsWith('event: ')); const dataLine = lines.find(l => l.startsWith('data: ')); if (eventLine && dataLine) { const event = eventLine.slice(7); const data = JSON.parse(dataLine.slice(6)); console.log(event, data); } } }
}
Python
import requests
import json url = "http://localhost:11023/v1/events?collection=blog"
headers = {"Authorization": "Bearer YOUR_TOKEN"} # optional with requests.get(url, headers=headers, stream=True) as resp: for line in resp.iter_lines(decode_unicode=True): if line.startswith("data: "): event = json.loads(line[6:]) print(f"{event['event']}: {event['collection']}/{event['key']}")
curl
curl -N http://localhost:11023/v1/events curl -N http://localhost:11023/v1/events?collection=blog curl -N -H "Authorization: Bearer eyJ..." http://localhost:11023/v1/events curl -N http://localhost:9000/events
Architecture
ββββββββββββββββ doc.added/updated/ β SSE Hub β GET /v1/events deleted events βββββΆβ (in-memory) ββββββ Client 1 (blog) β ββββββ Client 2 (all) handleAdd() β Broadcast ββββββ Client 3 (docs) handleDelete() βββββΆβ + Auth β batchHandler() β + IP limit β ββββββββββββββββ
- Events are generated at document write points (add, update, delete, batch)
- SSEHub broadcasts to all connected clients with auth/collection filtering
- Clients receive events via long-lived HTTP connection
- Auto-reconnect is built into the EventSource browser API