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:

EventTrigger
doc.addedNew document created
doc.updatedExisting document updated
doc.deletedDocument 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"}
FieldDescription
collectionFiltered collection (empty = all)
mode"read" or "readwrite" β€” indicates if user can write
userAuthenticated 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

ScenarioResult
No token401 Unauthorized
Token, no PermRead on collectionEvents for that collection are skipped
Token + PermReadEvents delivered, readOnly: true
Token + PermWriteEvents delivered, readOnly: false
Admin userAll 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:

SettingDefaultDescription
MDDB_SSE_MAX_CLIENTS1000Max total concurrent SSE connections
MDDB_SSE_MAX_PER_IP5Max 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 VarDefaultDescription
MDDB_SSE_ENABLEDtrueEnable/disable SSE endpoint
MDDB_SSE_MAX_CLIENTS1000Max total connections
MDDB_SSE_MAX_PER_IP5Max connections per IP

To disable SSE entirely:

MDDB_SSE_ENABLED=false ./mddbd

Endpoints

PortPathDescription
HTTP (11023)GET /v1/eventsMain SSE endpoint
MCP (9000)GET /eventsSSE 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

← Back to README | Configuration β†’ | Authentication β†’