Skip to content

Session Management

Manage server-sent event (SSE) sessions and client subscriptions in Mantis.

Server-Sent Events (SSE) provide real-time updates to Lens UI clients:

  • Deployment progress updates
  • Target status changes
  • System notifications
  • Audit events
mandible.toml
[sse]
# Single-use SSE token lifetime (seconds)
token_ttl_secs = 60
# SSE session lifetime in Redis (seconds)
session_ttl_secs = 3600
# Audience claim embedded in SSE tokens
audience = "mantis-sse"
# Redis connection used for SSE session/event fan-out
redis_url = "redis://127.0.0.1:6379"
# Redis key namespace (isolates multiple Mantis deployments on one Redis)
redis_namespace = "default"

For multi-instance Mandible deployments, point redis_url at a shared Redis. The SSE subsystem uses the same [sse] table — there is no separate [sse.redis] section.

mandible.toml
[sse]
redis_url = "redis://:password@redis:6379"
redis_namespace = "default"
session_ttl_secs = 3600
Terminal window
# Standalone override read directly by Mandible
SSE_REDIS_URL="redis://:password@redis:6379"
# Or the prefixed config-loader form
MANDIBLE__SSE__REDIS_URL="redis://:password@redis:6379"
MANDIBLE__SSE__REDIS_NAMESPACE="default"
MANDIBLE__SSE__TOKEN_TTL_SECS=60
MANDIBLE__SSE__SESSION_TTL_SECS=3600

SSE streams are nested under /api/v1/sse:

  • GET /api/v1/sse/{resource_type} — stream events for a resource type (list view)
  • GET /api/v1/sse/{resource_type}/{id} — stream events for a specific entity (detail view)

Valid resource_type values include deployments and targets.

SSE connections are not authenticated with the regular Authorization: Bearer JWT header (which would leak the token into server logs and proxy access logs). Instead, the client first exchanges its session for a short-lived, single-use SSE token, then passes it as the sse_token query parameter:

Terminal window
# 1. Obtain a single-use SSE token (uses the authenticated session/JWT)
SSE_TOKEN=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/auth/sse-token" | jq -r '.sse_token')
# 2. Open the event stream with the token as a query parameter
curl -N "https://api.mantis.local/api/v1/sse/deployments?sse_token=$SSE_TOKEN"

To stream events for a single entity, append its ID:

Terminal window
curl -N "https://api.mantis.local/api/v1/sse/deployments/abc123?sse_token=$SSE_TOKEN"

The SSE stream emits nameless events (no event: field) whose data: payload is a JSON-serialized entity event:

data: {"type":"updated","resource_type":"deployments","resource_id":"abc123","data":{...},"changed_fields":["status"],"timestamp":"2024-01-01T12:00:00Z"}
data: {"type":"created","resource_type":"targets","resource_id":"42","data":{...},"timestamp":"2024-01-01T12:00:00Z"}

type is created, updated, or deleted; resource_type is the entity kind (deployments, targets, …). Switch on those fields to react to changes.

The only named events are connected (sent on stream open, payload {}) and lagged (sent when the broadcast buffer overflows, payload {"missed_events": N}).

The SSE subsystem is configured by the single [sse] table (there is no [sse.redis] section and no enabled flag). Its fields are token_ttl_secs, session_ttl_secs, audience, redis_url, and redis_namespace:

mandible.toml
[sse]
token_ttl_secs = 60
session_ttl_secs = 3600
audience = "mantis-sse"
redis_url = "redis://:password@redis:6379"
redis_namespace = "mantis:sse"
Key PatternTypeTTLDescription
sse:sessions:{namespace}:{session_id}Stringsession_ttlJSON-serialized session data (one key per session)

The {namespace} segment is the redis_namespace config value (default: default), so the default prefix is sse:sessions:default:.

Sessions are stored as a JSON string via SET … EX:

GET sse:sessions:default:sess_123
# Returns a JSON object, e.g.:
# {"user_id":"...","username":"alice","email":"alice@example.com","roles":["admin"],"tenant_id":"...","refresh_token":"...","expires_at":1704153600,"revoked":false}

For multi-instance Mandible with SSE:

nginx.conf
upstream mandible {
ip_hash; # Sticky sessions by client IP
server mandible-1:3000;
server mandible-2:3000;
}

If using Redis session storage, any instance can serve events:

# kubernetes ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# No sticky session needed with Redis
nginx.ingress.kubernetes.io/proxy-read-timeout: '3600'
nginx.ingress.kubernetes.io/proxy-send-timeout: '3600'
frontend http
bind *:443 ssl crt /etc/ssl/mantis.pem
acl is_sse path_beg /api/v1/sse
use_backend mandible_sse if is_sse
default_backend mandible
backend mandible_sse
balance source # Sticky by source IP
option http-server-close
timeout server 3600s
server mandible-1 10.0.0.1:3000 check
server mandible-2 10.0.0.2:3000 check
backend mandible
balance roundrobin
server mandible-1 10.0.0.1:3000 check
server mandible-2 10.0.0.2:3000 check

SSE connections use periodic keep-alive comments sent by Axum’s KeepAlive mechanism. The keep-alive is a bare SSE comment line with no event name or data payload — browsers receive it silently to prevent the connection from being closed by proxies or firewalls:

:

The interval is 3 seconds (SSE_KEEPALIVE_SECS). There is no event: heartbeat field; the comment line is invisible to EventSource.onmessage handlers.

Clients should handle reconnection:

const eventSource = new EventSource(
`/api/v1/sse/deployments?sse_token=${sseToken}`
);
eventSource.onerror = () => {
// Reconnect with exponential backoff
setTimeout(
() => {
eventSource.close();
// Create new connection
},
Math.min(1000 * Math.pow(2, retryCount), 30000)
);
};

The server does not set id: fields on SSE events, so the browser’s Last-Event-ID tracking has nothing to send and no resume point is available server-side. On reconnection, the client must obtain a fresh SSE token and re-open the stream from the current server state:

eventSource.onerror = () => {
eventSource.close();
// Fetch a new SSE token, then re-open EventSource
};
Terminal window
# List all sessions (use SCAN in production to avoid blocking)
redis-cli SCAN 0 MATCH "sse:sessions:default:*" COUNT 100
# Read a specific session (returns JSON string)
redis-cli GET "sse:sessions:default:sess_123"

There are no dedicated SSE Prometheus metrics at present. Monitor SSE session counts via Redis (see the redis-cli examples above) and watch Mandible’s structured logs for connection lifecycle events.

Mandible’s health endpoint reports only overall liveness/readiness (database connectivity); it does not expose an SSE sub-object:

Terminal window
curl -s https://localhost:3000/api/v1/health
{
"status": "ok"
}

SSE session/token lifetimes are bounded by session_ttl_secs and token_ttl_secs in the [sse] table. (There are no max_sessions_per_tenant / max_total_sessions settings — those keys are not part of SseConfig.)

SSE connections consume memory:

ResourcePer Connection
Connection state~1 KB
Event buffer~10 KB
Total~11 KB

For 1000 connections: ~11 MB

For high-volume deployments, consider:

  1. Event aggregation: Batch small updates
  2. Subscription filtering: Only send relevant events
  3. Rate limiting: SSE event throttling is not configurable via [sse] (there are no event_rate_limit / aggregation_window_ms keys). The lagged control event signals when a slow consumer overflows its buffer.

SSE endpoints require a short-lived, single-use SSE token (not the regular JWT header). Obtain it from POST /api/v1/auth/sse-token using the authenticated session, then pass it as the sse_token query parameter:

Terminal window
SSE_TOKEN=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/auth/sse-token" | jq -r '.sse_token')
curl -N "https://api.mantis.local/api/v1/sse/deployments?sse_token=$SSE_TOKEN"

Events are filtered by tenant:

  • Users only receive events for their tenant
  • Session storage is namespaced by tenant
  • Cross-tenant access is prevented

SSE tokens are short-lived (token_ttl_secs) and validated at connection time; the stream is bounded by session_ttl_secs. (There are no revalidate_interval_secs / disconnect_on_token_expiry settings — these are not part of SseConfig.)

  1. Confirm Mandible is reachable and healthy:

    Terminal window
    curl -s https://localhost:3000/api/v1/health
  2. Verify proxy timeouts:

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
  3. Check for buffering issues:

    proxy_buffering off;
  1. Confirm the resource type is valid (e.g. deployments, targets) — an unknown resource_type is rejected before any events are streamed.

  2. Verify the stream opens with a fresh SSE token (tokens are single-use):

    Terminal window
    curl -s -N "https://localhost:3000/api/v1/sse/deployments?sse_token=$SSE_TOKEN" -v
  1. Check Redis connectivity:

    Terminal window
    redis-cli -a password ping
  2. List all sessions (use SCAN in production):

    Terminal window
    redis-cli SCAN 0 MATCH "sse:sessions:default:*" COUNT 100
  3. Check session TTL (replace default with your redis_namespace if changed):

    Terminal window
    redis-cli TTL "sse:sessions:default:sess_123"
  1. Check connection count via Redis (use SCAN in production):

    Terminal window
    redis-cli SCAN 0 MATCH "sse:sessions:default:*" COUNT 100
  2. Lower the session TTL so idle sessions expire sooner:

    [sse]
    session_ttl_secs = 1800