Context Platform
Documentation

Version Latest

Real-time feed and presence

Subscribe to the organisation's ordered Context event feed over Server-Sent Events with resume, and publish presence in spaces and pair sessions.

The first-party apps learn that a source was re-indexed, a lens was shared or a pair session changed without polling. Govern keeps one ordered event feed per organisation and streams it to each viewer, filtered by what that viewer may read now. PostgreSQL is the only moving part: no broker is needed, so the feed works in an air-gapped, single-database installation.

Events carry ids, counts and short codes only: never a chunk, a file, a lens name, a query or a message. An app reads the content through the normal, authorised routes.

Open a stream

curl -N -H "Authorization: Bearer $TOKEN" -H "Accept: text/event-stream" \
  "https://<your Govern host>/api/v1/context/events?spaceId=<optional>"
  • spaceId limits the stream to one space (the caller must be able to read it, else 404).
  • Resume with the Last-Event-ID header (or lastEventId); a browser EventSource does it by itself.
  • 422 for a malformed value, 429 context.events.too_many_connections over the connection caps, 503 while the server shuts down.

Stream format

SSE eventdata
ready{lastEventId, latestEventId, replaying, heartbeatSeconds, maxAgeSeconds}
a feed type{id, type, spaceId?, spaceIds?, lensId?, sessionId?, subjectId?, actor?, at?, meta?, redacted?}
presence.updated, presence.left{type, userId, spaceId, sessionId?, lensId?, state?, expiresAt?}
heartbeat{at, latestEventId} every 30 seconds
error{code, message, retryable, latestEventId?}; the server closes the stream after it

A : keep-alive comment is written every 10 seconds.

Ordering and resume

  • Event ids are per organisation, strictly increasing and gap-free in the feed. A stream delivers only the events the caller may read, so a client sees gaps in the ids it receives.
  • Everyone sees the same total order, and no event is skipped. That order is the order in which events were relayed, which with several replicas can differ from the order the changes committed. Treat an event as "this changed, here are its ids" and reconcile with the object's version or by reading it.
  • Reconnecting with the last id replays exactly what was missed, without loss or duplicates, within 1000 events and 24 hours. Further back, the stream sends context.events.resume_gap: reload your state and connect without an id.

Error codes

CodeMeaning
context.events.slow_consumerThe client did not keep up; reconnect with the last id
context.events.access_revokedAccess to the space or the sign-in ended (not retryable)
context.events.resume_gapThe events after Last-Event-ID are no longer available
context.events.shutting_down, context.events.reconnectThe server is stopping, or the stream reached its maximum age (15 minutes); reconnect with the last id
context.events.authz_unavailable, context.events.internalRetryable

Who receives what

Event familyDelivered when the viewer
source.*, snapshot.created, handoff.created, memory.*, project.repositories_changedMay read the space
lens.*May see the lens; spaceIds lists only the lens's spaces the viewer may read
session.*May read the session's space and takes part in the session
agentfile.*Owns the personal file, or may read the node of any other file
work.item.*May see the work item
overlap.*Owns one of the two items
share.*May see the lens

Events of a restricted space carry redacted: true, the type and the ids only. Authorisation decisions are re-checked every 10 seconds (KEYSTONE_CONTEXT_EVENTS_REFRESH), so a revocation takes effect within that time; a stream bound to a lost space ends with access_revoked, and so does one whose sign-in stopped being valid. A permission removed from a role reaches a running stream when it reconnects, at the latest after 15 minutes.

Feed types

source.indexed, source.stale, source.deleted, snapshot.created, handoff.created, project.repositories_changed, lens.created, lens.updated, lens.shared, lens.unshared, lens.frozen, session.started, session.joined, session.left, session.ended, session.lens_changed, session.sources_changed, memory.created, memory.accepted, memory.superseded, memory.archived, agentfile.created, agentfile.updated, agentfile.pending, agentfile.rejected, agentfile.archived, agentfile.hold, work.item.declared, work.item.updated, work.item.finished, work.item.abandoned, overlap.opened, overlap.updated, overlap.ended, and share.requested, share.approved, share.rejected, share.cancelled, share.expired, share.refused.

Session invitations publish no event: an invitee finds them with GET /api/v1/context/sessions/invited.

Presence

PUT /api/v1/context/presence
{"spaceId": "<id>", "sessionId": "<optional>", "lensId": "<optional>", "state": "active"}

The answer carries expiresAt and ttlSeconds; heartbeat every ttlSeconds / 3. DELETE /api/v1/context/presence?spaceId=&sessionId= leaves at once.

  • state is a short token (^[a-z][a-z0-9_-]{0,31}$, default active), never free text.
  • Presence is ephemeral: it is never audited and never stored in the feed. Entries expire 30 seconds after the last heartbeat (KEYSTONE_CONTEXT_PRESENCE_TTL) and the table is empty after a database failover; it refills within a heartbeat.
  • The collaboration policy's presence switch applies (403 context.collab_policy.capability_disabled).

Deployment notes

  • A reverse proxy must not buffer the stream or time it out sooner than the keep-alive. The server sets X-Accel-Buffering: no; raise the proxy's read timeout above the heartbeat (30 seconds).
  • Browsers allow about six HTTP/1.1 connections per origin: open one stream per app, not per component. HTTP/2 lifts the limit.
  • Connection caps are per replica: 5 streams per user and 200 per organisation by default.
  • If a replica's PostgreSQL LISTEN connection drops, delivery falls back to polling (about 7 seconds at worst) until it reconnects. Selfguard does not watch this fallback yet; the log line is "LISTEN connection lost".
  • A malformed KEYSTONE_CONTEXT_EVENTS_* value turns only these routes into 503 context.events.unavailable; retrieval is unaffected.

Metrics: keystone_context_events_streams, keystone_context_events_published_total, keystone_context_presence_published_total, keystone_context_events_slow_consumers_total, keystone_context_events_pull_errors_total, keystone_context_events_skipped_total.

Last reviewed 17 October 2026View as Markdown

Real-time feed and presence · Docs · Neural Inverse