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

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