Activity Feed Realtime Frames

The realtime frame contract in activity-feed-api is designed for fast UI invalidation and scoped updates, not full state replication. Integrate it as an event signal layer and refetch authoritative data from domain APIs.


Conceptual Model

activity-feed-api does two things at once:

  1. It materializes selected high-value events into persisted feed items.
  2. It fans out realtime frames for a broader set of events over SSE.

Important: many realtime frames are intentionally invalidation-only and do not create feed rows.

Envelope Format You Receive

Activity-feed realtime data frames are CloudEvents-style envelopes delivered over SSE.

  • The SSE frame data: body is a JSON event envelope.
  • The type field is the primary event discriminator you should switch on.
  • Delivery authorization is evaluated from envelope fields such as ehorgid, ehprojectid, ehuserid, and ehvisibility.

For the CloudEvents standard, see: https://cloudevents.io/

Event shape assumptions for client code

You can safely assume:

  • type is always present on data frames.
  • ehorgid and ehvisibility are present on deliverable realtime envelopes.
  • sequence fields (ehglobalseq, ehorgseq, optional project/session variants) are used for cursor advancement and replay.

You should not assume:

  • every event includes every optional field (subject, ehprojectid, ehuserid),
  • every event is relevant to the current page context,
  • event data payload shape is stable across all event domains unless your integration is pinned to a specific event contract.

What You Can Expect

Guaranteed integration expectations

  • Org-scoped realtime access with authenticated caller identity.
  • Optional project/session subscriptions for narrowed delivery.
  • Heartbeat and reconnect-friendly control frames.
  • A replay strategy based on Last-Event-ID cursor state.

Feed persistence expectations

High-value event types are persisted into feed history, including examples like:

  • workflow run created/completed/failed,
  • workflow approval pending/resolved,
  • repository snapshot changed,
  • governance approval changed,
  • knowledge page created/deleted,
  • artifact changed.

These appear in feed endpoints such as inbox/project timelines and support unread cursor flows.

Realtime-only events

Some event types are broadcast for invalidation but intentionally do not materialize into feed rows. Use the matrix below (Materialized Into Feed Item) to know which events affect timeline history versus realtime cache invalidation only.

What You Should Not Expect

  • Full domain payloads for every frame.
  • Strictly ordered, exactly-once business processing semantics in the browser.
  • Realtime frames as a replacement for querying domain APIs.
  • Every frame becoming a persisted feed item.

Treat realtime as a low-latency hint channel and make consumers idempotent.

Activity Feed Realtime Event Matrix

This page keeps no list of event types. The event catalogue is the source of truth, filtered to your organisation: see the event catalogue (GET /events/catalogue, or GET /events/catalogue/asyncapi for an AsyncAPI document). Whether an event becomes a FeedItem row is set by that event's descriptor feed metadata.

Frame Handling Pattern

1) Open unified stream

ts
1const stream = createAuthenticatedSseClient({
2  url: '/realtime/stream',
3  orgId,
4  getToken,
5  lastEventId: loadCursorMap(),
6});

2) Subscribe to project/session scopes as needed

ts
1await fetch(`/realtime/streams/${connectionId}/subscriptions`, {
2  method: 'POST',
3  headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
4  body: JSON.stringify({ scope: 'project', id: projectId }),
5});

3) Apply invalidation, then refetch authoritative state

ts
1stream.onMessage((envelope) => {
2  switch (envelope.type) {
3    case 'workflow.run.completed':
4    case 'artifact.row.changed':
5    case 'kb.page.created': {
6      invalidateRelevantQueries(envelope);
7      break;
8    }
9    default: {
10      // Unknown events should be ignored safely.
11      break;
12    }
13  }
14});

4) Parse as CloudEvents envelope first, then route by type

ts
1stream.onMessage((envelope) => {
2  if (typeof envelope.type !== 'string') return;
3
4  // Defensive checks for scope-sensitive experiences.
5  if (envelope.ehvisibility === 'project' && envelope.ehprojectid !== currentProjectId) {
6    return;
7  }
8
9  routeRealtimeEvent(envelope.type, envelope);
10});

Control Frame Behavior

Common control events to handle explicitly:

  • connected: capture connectionId and server cursors.
  • heartbeat: reset liveness watchdog.
  • subscribed/unsubscribed: confirm scope changes.
  • scope_denied: keep UI stable and show access notice.
  • resync: clear stored cursors and reconnect with full refetch.
  • goodbye: close gracefully and reconnect with backoff.

Swagger Gap and Consumer Guidance

Stream payload details (control frames, envelope fields, replay cursors, and materialized-vs-realtime-only distinctions) are not fully represented in Swagger schemas. For implementation correctness, treat this page plus the streaming guide as the primary contract for realtime consumption behavior.

Example: Resync Recovery

ts
1stream.onControl('resync', () => {
2  clearRealtimeCursorState();
3  invalidateAllCriticalQueries();
4  stream.reconnect({ lastEventId: '{"global":"0"}' });
5});

Example: Feed + Realtime UX

A practical pattern for activity surfaces:

  1. Load feed inbox/project list via feed endpoints.
  2. Open unified realtime stream.
  3. On frame arrival, invalidate only affected query keys.
  4. Keep unread counters driven by cursor updates and feed refetch.
  5. On disconnect, reconnect with jittered backoff and stored cursor map.

This gives responsive UX without over-fetching or coupling UI state to raw frame internals.


Previous: ← Streaming and SSE Guide Next: Knowledge Base API Guide →