ADR-006: Notifications WebSocket
- Status: Accepted
- Date: 2026-04-16
- Author: CTO Agent (KALA-133)
Context
AgentCore already had WebSocket infrastructure for agent-tasks progress. New
product flows needed real-time user notifications for approvals, mentions, and
generated documents. The team had to choose between extending the existing
WebSocket plugin, adding Server-Sent Events, or relying only on REST polling.
Decision
Add /ws/notifications to the existing Fastify WebSocket plugin.
The notification channel:
- authenticates with the same JWT model as REST;
- supports a query token or first-message auth;
- subscribes to notification events for the authenticated
userId; - sends
notification.createdevents when producers callemitNotification(); - keeps durable state in the
notificationstable.
Alternatives Considered
REST polling only
Rejected. Polling is simple, but it adds latency to approval and document-ready workflows and increases request volume for active dashboards.
Separate SSE endpoint
Rejected for now. SSE is a good fit for server-to-client events, but the app already has authenticated WebSocket plumbing and tests. Adding another transport would duplicate auth and lifecycle logic.
Extend /ws/agent-tasks
Rejected. Agent task progress is namespace/task scoped. Notifications are user-inbox scoped. Keeping separate paths avoids mixing subscription semantics.
Consequences
Positive
- Reuses the existing WebSocket plugin and JWT verification path.
- Keeps notification delivery tied to current user identity.
- REST inbox remains the source of truth after reconnects.
- Tests can exercise the socket with the same injected WebSocket helpers.
Negative
- The current EventEmitter broadcast is process-local.
- Multi-replica production needs Redis pub/sub or another shared event bus.
- Clients still need REST sync for missed events while disconnected.
Implementation Notes
src/lib/notifications.tsownsemitNotification()andsubscribeUserNotifications().src/plugins/websocket.tsregisters/ws/notifications.- Producers should create the database row first, then emit the event.
- The socket closes with code
4001on auth timeout or invalid token.