Skip to content

Commit 7c0ed55

Browse files
gmoz22Copilot
andcommitted
feat: real-time WebSocket notifications template
Serverless backend + React MFE for pushing EventBridge events to browser clients in sub-second latency, with missed-event replay and live resubscription — without polling or per-user queues. Backend (template-websocket-service): - EventBridge → broadcast Lambda (aws-lambda-stream pipeline) → API Gateway WebSocket push; events persisted to EventsTable with TTL - WebSocket lifecycle: connect, disconnect, replay, subscribe handlers - SubscriptionsTable supports per-type and catch-all (*) subscriptions - Missed-event replay on reconnect via client-triggered replay route - Live resubscription without reconnecting via subscribe route - Cross-region forwarding via EventBridge rule (opt-in, commented out) - Transformation logic (toMessage, toConnections, toUpdateRequest) extracted to src/models/websocket.js following template-bff-service pattern - Full unit test coverage (c8 + Mocha + Chai) across broadcast, connections, and models Frontend (template-websocket-mfe): - useWebSocket hook: connects, auto-reconnects with exponential backoff, handles live resubscription and opt-in missed-event replay via sessionStorage - App, ConnectionStatus, EventLog, TagInput components - Single-SPA lifecycle integration, Vite build, dark console UI Co-authored-by: Copilot <copilot@github.com>
1 parent e878611 commit 7c0ed55

48 files changed

Lines changed: 22298 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
File renamed without changes.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# template-websocket-notifications
2+
3+
A complete WebSocket notification system for pushing real-time events from the backend to browser clients.
4+
5+
---
6+
7+
## Folders
8+
9+
| Folder | Description |
10+
|--------|-------------|
11+
| [`template-websocket-service`](template-websocket-service/README.md) | Serverless backend (Node.js 22, SLS v4) — bridges EventBridge events to WebSocket clients via API Gateway |
12+
| [`template-websocket-mfe`](template-websocket-mfe/README.md) | React micro-frontend (Vite, single-spa) — connects to the service, displays live events |
13+
14+
---
15+
16+
## Purpose
17+
18+
A lightweight real-time push channel from backend services to browser clients. Any backend event published to EventBridge can be delivered to subscribed WebSocket connections within sub-second latency, with no polling, no per-user queues, and no frontend infrastructure beyond a single React hook.
19+
20+
---
21+
22+
## Use cases
23+
24+
### Async job completion
25+
26+
An MFE kicks off a long-running backend operation and needs to know when it's done.
27+
28+
- An optimization job completes
29+
- A validation job finishes processing
30+
- A bulk import/export operation is ready for download
31+
32+
### Live notifications
33+
34+
Multiple users need to be informed of something in real time.
35+
36+
- Alerts, warnings, or system announcements pushed to all connected users
37+
- A record was updated by another user and open viewers should be notified
38+
- Operational status changes that require immediate attention
39+
40+
---
41+
42+
## How it differs from an SQS + SNS polling approach
43+
44+
An alternative architecture uses per-user SQS queues subscribed to SNS topics, with the frontend long-polling each queue. That approach is designed for **continuous high-volume data sync**.
45+
46+
This WebSocket approach is designed for **sparse operational notifications** where simplicity and low latency matter more than durability.
47+
48+
| | WebSocket Notifications (this) | SQS + SNS Polling |
49+
|---|---|---|
50+
| **Use case** | Job done, status change, one-off alerts | Continuous data sync |
51+
| **Latency** | Sub-second | 1–5 seconds |
52+
| **Message durability** | TTL window only (configurable, default 30 min) | Queued until consumed |
53+
| **Per-client filtering** | SubscriptionsTable (server-side) | SNS filter policies |
54+
| **Infrastructure** | 3 DynamoDB tables + API GW WebSocket + Lambda | Per-user SQS queues + SNS + cleanup jobs |
55+
| **Ops overhead** | TTL handles all cleanup | Cleanup jobs + multi-region queue management |
56+
| **Multi-region** | EventBridge cross-region forwarding | Multi-region SQS replication |
57+
| **Cost model** | Pay per message + connection-minutes | Pay per SQS poll request (constant) |
58+
| **FE integration** | Single `useWebSocket` hook | Polling worker + client-side data management |
59+
60+
**Rule of thumb:** If you need data to flow continuously into the frontend for queries and rendering, an SQS + SNS approach may be more appropriate. If you need to notify the UI that something happened, use this.
61+
62+
---
63+
64+
## Architecture
65+
66+
```
67+
┌─────────────────────────────────────────────────────────────────────────┐
68+
│ Backend (per region) │
69+
│ │
70+
│ EventBridge ──────► broadcast Lambda (aws-lambda-stream) │
71+
│ (detail-type: │ │
72+
│ thing-updated) ├─► query SubscriptionsTable (type + catch-all *) │
73+
│ │ └─► push to each matched connection │
74+
│ └─► persist to EventsTable (TTL 30 min) │
75+
│ │
76+
│ EventBridge ──────► CrossRegionForwardRule ──► other region bus │
77+
│ │
78+
│ Client $connect ──► connect Lambda │
79+
│ ?subscribe= ├─► write ConnectionsTable │
80+
│ thing-updated └─► write SubscriptionsTable (one row per type) │
81+
│ │
82+
│ ws.onopen ────────► replay Lambda { action: 'replay', since } │
83+
│ (replay: true only) └─► query EventsTable → push missed events │
84+
│ │
85+
│ Sub change ────────► subscribe Lambda { action: 'subscribe', │
86+
│ (live resub) | eventTypes: [...] } │
87+
│ ├─► delete old subscriptions for connectionId │
88+
│ └─► write new SubscriptionsTable rows │
89+
│ │
90+
│ Client $disconnect ► disconnect Lambda │
91+
│ ├─► delete ConnectionsTable │
92+
│ └─► delete SubscriptionsTable (all for this conn) │
93+
└─────────────────────────────────────────────────────────────────────────┘
94+
95+
┌─────────────────────────────────────────────────────────────────────────┐
96+
│ Frontend (MFE) │
97+
│ │
98+
│ useWebSocket hook │
99+
│ ├─► opens wss://{url}?subscribe=thing-updated │
100+
│ ├─► on open: sends { action: 'replay', since } if replay: true │
101+
│ ├─► on sub change: sends { action: 'subscribe', eventTypes } │
102+
│ ├─► receives pushed events in real time │
103+
│ └─► auto-reconnects with exponential backoff on drop │
104+
└─────────────────────────────────────────────────────────────────────────┘
105+
```
106+
107+
---
108+
109+
## Publishing events from your backend
110+
111+
Any Lambda in your system can trigger a WebSocket notification by publishing to the EventBridge bus:
112+
113+
```js
114+
import { publishToEventBridge } from 'aws-lambda-stream';
115+
116+
// In your pipeline rule:
117+
{
118+
id: 'notifyCompletion',
119+
flavor: cdc,
120+
eventType: 'job-completed',
121+
toEvent: (uow) => ({
122+
type: 'job-completed',
123+
id: uow.event.id,
124+
tags: { region: process.env.AWS_REGION },
125+
result: uow.event.result,
126+
}),
127+
}
128+
```
129+
130+
Include `tags.region` in all published events if you plan to enable multi-region — the cross-region forward rules use it to prevent forwarding loops.
131+
132+
---
133+
134+
## What this is NOT
135+
136+
- Not a replacement for an SQS + SNS polling system — use that for continuous data sync
137+
- Not a message queue — if the TTL window expires, events are gone
138+
- Not for high-frequency data updates — designed for sparse notifications
139+
- Not for sensitive data without additional auth validation at `$connect` time
140+
141+
---
142+
143+
## Going further
144+
145+
- [template-websocket-service/README.md](template-websocket-service/README.md) — Lambda functions, DynamoDB schema, subscription semantics, multi-region setup, deployment
146+
- [template-websocket-mfe/README.md](template-websocket-mfe/README.md) — hook API, components, Vite config, single-spa integration
147+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Full WebSocket URL including protocol (e.g., wss://api-id.execute-api.region.amazonaws.com/stage)
2+
WS_URL=
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
extends: [
3+
'plugin:react/recommended',
4+
'plugin:jest/recommended',
5+
],
6+
parser: '@babel/eslint-parser',
7+
plugins: ['react', 'react-hooks', 'jest'],
8+
env: {
9+
browser: true,
10+
node: true,
11+
'jest/globals': true,
12+
},
13+
parserOptions: {
14+
ecmaVersion: 2020,
15+
sourceType: 'module',
16+
ecmaFeatures: { jsx: true },
17+
requireConfigFile: false,
18+
},
19+
rules: {
20+
'react/react-in-jsx-scope': 0,
21+
'react/prop-types': 0,
22+
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
23+
'no-unused-vars': [1, { vars: 'local', args: 'none' }],
24+
},
25+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
coverage/

0 commit comments

Comments
 (0)