WebSockets (live fight updates)
The client maintains a WebSocket connection so the Profile screen can refresh live/pending fights without polling when someone accepts or rejects a fight.
This is separate from push notifications (FCM/APNs) and from the in-app bet_service FastAPI socket at /ws/{user_id}/{device_id} (used elsewhere in backend tests/services). The mobile/web app connects to the API Gateway WebSocket endpoint configured by REACT_APP_WS_URL.
Client implementation
| Piece | Path |
|---|---|
| URL builder + hook wrapper | client/src/components/websocket/WebSocketClient.js |
| Consumer (only current usage) | client/src/components/profile/Profile.js |
| Dependency | react-use-websocket |
Profile calls buildWebSocket({ userId: profileData?.id, authToken: bearerToken }) and watches lastMessage. When a relevant event arrives, it runs refreshPendingFights() (same path as pull-to-refresh).
Connection URL
Set in production builds via REACT_APP_WS_URL (see ios_deployment / env for native):
| Environment | Typical URL |
|---|---|
| Production | wss://ws.getfoodfight.link/ |
| Dev | wss://ws.ff-devs.click/ |
WebSocketClient builds the final URL as:
- Base:
serverUrlargument orprocess.env.REACT_APP_WS_URL - Query:
user_id,jwt_token(Cognito ID token from auth context)
On https pages, ws: is upgraded to wss:. Local http + wss:// logs a scheme mismatch warning (see Troubleshooting).
Client behavior
From WebSocketClient.js:
- Reconnect: up to 5 attempts, exponential backoff (max 10s)
- Heartbeat: JSON
{ "type": "ping" }every 30s (idle timeout avoidance) - Shared connection:
share: true(one socket per URL across hook instances) - Logging:
[WebSocketClient]prefix on open / message / close / error
Message types handled on Profile
The app parses JSON from lastMessage.data and refreshes fights when:
type | Who gets a refresh |
|---|---|
fight.accepted | maker_id or taker_id matches current profile user |
fight.rejected or fight_rejected | maker_id matches current profile user |
Example server payload shape for accept (from bet_service):
{
"type": "fight.accepted",
"bet_id": 123,
"maker_id": "...",
"taker_id": "...",
"maker_outcome": "...",
"taker_name": "...",
"maker_items": "...",
"taker_items": "...",
"maker_cost": 0,
"taker_cost": 0
}
Other event types may be sent by the backend but are not handled on Profile today—extend Profile.js if you add new real-time UI.
Backend (high level)
sequenceDiagram
participant App as Client Profile
participant WS as API Gateway WebSocket
participant Lambda as $connect / fightEvent
participant DB as websocket_connections
participant Bet as bet_service
App->>WS: Connect ?user_id & jwt_token
Lambda->>DB: Store connectionId
Bet->>Lambda: fight accepted / rejected
Lambda->>WS: postToConnection
WS->>App: JSON message
App->>App: refreshPendingFights()
| Layer | Location |
|---|---|
| Connect / disconnect / route events | backend/projects/websocket/ (websocket_handler.py, websocket_server.py) |
Persist connectionId per user | libs/db — WebSocketConnection / websocket_repo.py |
| Push to connected clients | libs/websockets/src/libs/websockets/util.py (send_websocket_message_to_user, notify_maker_and_taker) |
| Emit on accept | bet_service — create_fight_accepted_message_payload in util.py, live_bet_service.py |
Auth on connect: Cognito JWT in jwt_token query param (verified in websocket handler).
Local development
Client
- Set
REACT_APP_WS_URLinclient/.envto your dev WebSocket URL (or rely on native build env from CI). - Open Profile while logged in; watch console for
[WebSocketClient].
Backend
cd backend
make websocket-service # WebSocket service + DB
make websocket-tests # BDD component tests
Bet-service integration tests often use --profile websocket (see backend/Makefile). Local CIO/WS mocking may use Wiremock profiles in compose.yaml.
Adding a new consumer
- Import
buildWebSocketfromcomponents/websocket/WebSocketClient.js. - Pass
userIdand CognitoidToken(same as Profile’sbearerToken). - Handle
lastMessagein auseEffect—must follow Rules of Hooks;buildWebSocketwrapsuseWebSocketand must be called unconditionally at component top level (same pattern asProfile.js).
Prefer a single shared connection (share: true is already default) rather than opening one socket per screen.
Related docs
- Troubleshooting — WebSockets
- Frontend overview — feature map
- Historical design / approval notes: App WebSockets (contributing)