Skip to content

Realtime

Realtime is implemented as a Feathers v6 SSE subsystem inside apps/api. It is mounted through the existing Mastra API server route at /realtime/sse; it is not a Mastra adapter and not a separate app.

  • apps/api owns the Feathers app and Better Auth handshake.
  • apps/web owns the Feathers client plugin and same-origin /api/realtime/sse proxy.
  • Redis pub/sub fans events across API replicas through REALTIME_REDIS_CHANNEL.
  • Normal local bun run dev uses the Docker Redis service from bun run docker:up, so browser and agent-browser verification exercise the Redis-backed fanout path. A single-process non-production stack may publish directly through the in-process Feathers runtime only when Redis is absent and background connector runtimes are disabled. Production, customer-cluster, and multi-replica runtimes still require Redis fanout.
  • Shared contracts live in packages/contracts/src/realtime/events.ts.
  • Pinia stores are the durable browser-side sinks for connector, Library job/readiness, shell, chat/sidebar, and notification realtime state.
  • The Feathers runtime owns one app-level publish listener plus per-connection subscriber registry entries. Tests prove subscribers are removed on abort, body cancel, and runtime stop.
  • Listener-warning fixes must not weaken realtime privacy. User events are filtered before delivery through server-derived channel membership; the browser must never receive another user’s private event and rely on client-side filtering to hide it.
  • Recent chat sidebar and command-palette state comes from the shared chat summaries store; those compact surfaces do not refetch just because the route changes.
  • Shared-with-me sidebar state comes from the shared-chat summaries store and refreshes from the recipient’s user-scoped chat-share grant and revocation notifications.
  • Route-backed browse pages use the shared pending-update banner when realtime changes would move visible rows; compact surfaces such as the chat sidebar and notification menu remain live.
  1. API route handlers, queue workers, or schedulers persist domain state.
  2. API publishes a typed RealtimeEventEnvelope.
  3. Redis fans the event to every API replica. Only the documented explicit no-Redis, single-process development mode sends directly to Feathers.
  4. The Feathers runtime delivers the event only to server-owned channels such as user:${userId}.
  5. The web realtime plugin validates the inbound payload with contract schemas.
  6. Pinia store actions merge the update and trigger snapshot refresh on connect/reconnect.

User-targeted events must stay on server-resolved user channels. Browsers never request a channel join, and shared-chat notifications only refresh the recipient’s compact shared-chat snapshot.

  • No unauthenticated SSE connection.
  • Browser clients do not choose channels, users, groups, or resources.
  • Better Auth session helpers derive the user id during the Feathers handshake.
  • Listener or fanout optimizations must keep user-scoped authorization on the server side.
  • Internal targets are not sent to clients.
  • Safe global/system events are allowlisted.
  • There is no Socket.io compatibility path and no old event-name alias layer.
Family Events
Connectors connector-status-updated, connector-disconnect-started, connector-disconnect-failed
Chat sidebar and queue chat-thread-created, chat-thread-updated, chat-thread-deleted, chat-turn-queue-updated
Chat turn lifecycle chat-turn-started, chat-turn-completed, chat-turn-interrupted
HITL chat-hitl-updated
Library jobs library-job-updated
Notifications notification-created
Safe global status system-status-updated

Active assistant token streaming remains on the chat stream path. Do not route response deltas through Feathers.

  • Provider-backed Library upload readiness is implemented through library-job-updated. Canonical LibraryJob rows own job status/readiness; Upload records store upload-session/provider metadata. API publishes after persisted upload/indexing state changes.
  • Connector settings changes that toggle indexed mode proactively reconcile provider-backed upload readiness for the connector, keeping open Library pages on the Library job realtime path. Indexed-mode toggles delete only vector/indexed artifacts and preserve Library items, Upload records, and provider file metadata.
  • Library job snapshots are recovered over HTTP on initial load and reconnect. Snapshots include the compact Library readiness summary so missed-event recovery updates counts/status as well as job rows. The Library store guards both event and snapshot timestamps so an older recovery response cannot move a job backward or resurrect a hidden non-actionable upload row. The web app must not use steady-state interval polling for upload readiness.
  • Library-owned import, delete, and reindex producers use the same LibraryJob realtime/snapshot path. Parser/extraction progress and batch maintenance producers are still planned; keep those producer checkboxes unchecked in canonical specs until their async workflows exist.

Notifications are durable, user-scoped API records. The API derives the user from Better Auth; clients cannot supply user or channel targets.

  • GET /notifications lists the authenticated user’s notifications with pagination and readStatus=all|read|unread.
  • POST /notifications/:id/read, POST /notifications/:id/unread, POST /notifications/mark-all-read, and DELETE /notifications/:id mutate only records owned by the authenticated user.
  • The public DTO uses readAt: string | null and code-based copy; the frontend renders the English text.
  • Current producers are connector run completion, connector sync/indexing failures, shared-chat grants, and shared-chat revocations.
  • Connector completion notifications are limited to first/connect and manual runs that changed content. Successful runs create one combined notification after terminal success: updated and removed counts come from sync, searchable counts come from indexing, and live-only/indexing-off connectors record that searchable chat indexing is off. Automatic completion and no-change completion notifications are intentionally suppressed.
  • Connector failure notifications suppress repeated identical issues for the same connector so continuing errors do not spam the user.
  • Shared-chat grant notifications are created only for server-resolved newly granted viewers and link to the same shared-chat route used by the sidebar/history surfaces. Shared-chat grant removal and full share revocation notifications target former viewers without a shared-chat link, so open workspaces refresh shared-with-me state and remove revoked metadata.
  • The full notifications page uses URL-backed page/readStatus params and passes pagination state to the shared AppPageShell pagination slot, matching the all chats page behavior and keeping pagination bottom-aligned through authenticated-layout route meta.
  • Realtime notification-created events carry the validated notification DTO and are merged by the Pinia notifications store.
  • The header unread count and notification row unread chip use a shared presentation mapping. Failed connector sync/indexing notifications use the error accent; successful connector and chat-share lifecycle notifications use the default primary accent.
  • Mastra does not provide a native user-notification API in the installed version. Future Mastra background task lifecycle hooks should publish through the same app-owned notification service.

/notifications and /chats are route-backed browse pages. They show a shared “Load latest” banner when realtime events indicate newer rows are available, then refresh the HTTP snapshot after the user clicks it. This avoids changing row order or removing a row while someone is trying to click an item or row action.

The chat sidebar, command-palette recent chats, Shared with me sidebar, and notification menu are compact live surfaces. They read Pinia store state directly and use targeted snapshot refresh on connect/reconnect or user-scoped notification events instead of route-change reloads or interval polling.

Provider-backed uploads are Library-owned jobs because users wait for readiness in Library and in focused chat, not only on connector dashboard rows.

  • POST /uploads and PUT /uploads/:id/file persist upload/session metadata and canonical Library job state, then publish library-job-updated.
  • Connector embedding ingestion writes durable metadata/vectors first, then reconciles matching pending uploads and publishes the resulting Library job state.
  • Uploads tied to a connector without indexed ingestion enabled reconcile terminally instead of staying pending, but do not create Library attention or inline Library contents status rows because indexing was intentionally left off.
  • Toggling indexed mode back on makes new provider-backed uploads eligible for visible Library job readiness; old terminal non-actionable uploads are not silently reprocessed without a retry/reindex flow. Existing Library items remain visible across the off/on toggle.
  • Ordinary manual connector sync progress remains connector status, not per-Library-item status. It updates open Library pages only when the sync changes pending provider-backed upload readiness or ready Library item counts.
  • GET /libraries/:id/jobs is the snapshot route for Library job and compact readiness recovery.
  • Library itemCount counts only ready/focusable Library items. Pending uploads affect activeJobCount/updating; failed and actionable not-indexed uploads affect attention.
  • The Library detail page, Library index, focus picker, and focused-chat Sidekick consume the shared Library job/readiness store and the same workspace status-chip presentation used by connector status rows. Library detail renders one contents list: active, failed, and actionable not-indexed jobs appear inline, while ready jobs collapse into the durable Library item row they created.
  • Durable notifications are not created for routine upload progress; use the Library job state for in-context readiness.
  • Import, delete, and reindex jobs use the same status row presentation and stale-event handling. The web app does not add a new user action just to display these statuses.
Terminal window
bun test packages/contracts/src/realtime/events.test.ts
bun test packages/contracts/src/notifications/contracts.test.ts
cd apps/api && bun run test:connectors:realtime
cd apps/web && bun run test -- test/nuxt/ChatSummariesRealtime.nuxt.spec.ts test/nuxt/SharedChatSummariesStore.nuxt.spec.ts test/nuxt/RealtimeNotificationSharedChats.nuxt.spec.ts test/nuxt/UseConnectorStatusTransportRealtime.nuxt.spec.ts test/nuxt/NotificationsStore.nuxt.spec.ts test/nuxt/NotificationCenterButton.nuxt.spec.ts test/nuxt/NotificationListItem.nuxt.spec.ts test/nuxt/NotificationCopy.nuxt.spec.ts
cd apps/web && bun run test -- test/nuxt/LibraryJobsStore.nuxt.spec.ts test/nuxt/LibraryDetailRoute.nuxt.spec.ts test/nuxt/LibraryIndexRoute.nuxt.spec.ts test/nuxt/ConnectorResourcePicker.nuxt.spec.ts test/nuxt/ChatSidekickSections.nuxt.spec.ts

Use agent-browser to verify connector updates, Library upload readiness, chat/sidebar lifecycle, and notification menu/page behavior when local fixtures are available.

  • docs/features/realtime/spec.md
  • docs/features/realtime/decisions/ADR-001-feathers-v6-realtime-transport.md
  • packages/contracts/src/realtime/events.ts
  • packages/contracts/src/libraries/contracts.ts
  • packages/contracts/src/notifications/contracts.ts
  • apps/api/src/library/
  • apps/api/src/uploads/providerUploadService.ts
  • apps/api/src/notifications/
  • apps/api/src/realtime/
  • apps/web/app/plugins/realtime.client.ts
  • apps/web/app/stores/authenticated/storeLibraries.ts
  • apps/web/app/stores/authenticated/storeChatSummaries.ts
  • apps/web/app/stores/authenticated/storeSharedChatSummaries.ts
  • apps/web/app/stores/authenticated/storeNotifications.ts
  • apps/web/app/components/notifications/
  • apps/web/app/utils/notificationPresentation.ts
  • apps/web/app/composables/useRealtimeClient.ts
  • apps/web/app/composables/useRealtimeSubscription.ts