This document defines the stable websocket contract for backend/frontend collaboration based on the actual implementation in backend/src/gateway/events.gateway.ts.
- CORS: Enabled for all origins with credentials support
- Methods: GET, POST
- Protocol: Socket.IO over WebSocket
JWT token must be provided in one of these locations (checked in order):
socket.handshake.auth.tokensocket.handshake.headers.authorizationsocket.handshake.query.token
- Algorithm: HS256
- Secret: Configured via
JWT_SECRETenvironment variable - Validation: Signature verification + expiration check
- Payload Structure:
interface WsJwtPayload { sub?: string; // User ID (required for room access) userId?: string; // Alternative user ID field exp?: number; // Expiration timestamp iat?: number; // Issued at timestamp [key: string]: unknown; }
- Token extracted from handshake
- JWT format and signature validated
- Expiration checked
- On success:
client.data.userset to decoded payload - On failure: Connection immediately rejected with
client.disconnect(true)
- Pattern:
split:<splitId> - Scope: Room-scoped operations for split collaboration
- Authorization: Users must pass
AuthorizationService.canAccessSplit(userId, splitId)to join
- Join:
client.join(room)- Adds client to room - Leave:
client.leave(room)- Removes client from room - Broadcast:
server.to(room).emit(event, data)- Sends to all room members
All client events require JWT authentication and use WsJwtAuthGuard.
Purpose: Join a split's collaboration room
Payload Schema:
interface JoinSplitPayload {
splitId: string; // Required
}Validation:
splitIdis required- User must have access to the split via
AuthorizationService.canAccessSplit()
Response Schema:
interface JoinedSplitEvent {
splitId: string;
room: string; // "split:<splitId>"
}
interface WsHandlerResponse<T> {
event: "joined_split";
data: JoinedSplitEvent;
}Error Cases:
400 Bad Request: Missing or invalidsplitId401 Unauthorized: Not authenticated or no access to split
Purpose: Leave a split's collaboration room
Payload Schema:
interface LeaveSplitPayload {
splitId: string; // Required
}Response Schema:
interface LeftSplitEvent {
splitId: string;
room: string; // "split:<splitId>"
}
interface WsHandlerResponse<T> {
event: "left_split";
data: LeftSplitEvent;
}Error Cases:
400 Bad Request: Missing or invalidsplitId
Purpose: Get list of participants currently in a split room
Payload Schema:
interface SplitPresencePayload {
splitId: string; // Required
}Response Schema:
interface SplitPresenceEvent {
splitId: string;
participants: string[]; // Array of socket IDs
}
interface WsHandlerResponse<T> {
event: "split_presence";
data: SplitPresenceEvent;
}Error Cases:
400 Bad Request: Missing or invalidsplitId
Purpose: Broadcast activity to all participants in a split room
Payload Schema:
interface SplitActivityPayload {
splitId: string;
activity: SplitActivityData;
}
interface SplitActivityData {
type?: string; // Activity type
action?: string; // Action performed
actorId?: string; // User who performed action
description?: string; // Human-readable description
metadata?: Record<string, unknown>; // Additional data
timestamp?: string; // ISO timestamp
amount?: number; // Monetary amount if applicable
[key: string]: unknown; // Extensible properties
}Response Schema:
interface SplitActivityBroadcastEvent {
splitId: string;
activity: SplitActivityData;
}
interface WsHandlerResponse<T> {
event: "split_activity_broadcast";
data: SplitActivityBroadcastEvent;
}Broadcast: Also emits split_activity event to all room members with same payload
Error Cases:
400 Bad Request: MissingsplitIdoractivity
These events are emitted by the server to clients in specific split rooms.
Trigger: When a payment is processed for a split
Payload Schema:
interface PaymentReceivedEvent {
splitId?: string; // Split ID (optional, set by emitter)
paymentId?: string; // Payment identifier
participantId?: string; // Who made the payment
type?: string; // Payment type
amount?: number; // Payment amount
currency?: string; // Currency code
txHash?: string; // Transaction hash
asset?: string; // Asset type
timestamp?: string; // ISO timestamp
[key: string]: unknown; // Extensible properties
}Trigger: When split properties change
Payload Schema:
interface SplitUpdatedEvent {
splitId?: string; // Split ID (optional, set by emitter)
type?: string; // Update type
status?: string; // New status
changes?: Record<string, unknown>; // Changed properties
updatedAt?: string; // ISO timestamp
timestamp?: string; // ISO timestamp
amountPaid?: number; // Total amount paid
paymentId?: string; // Related payment ID
participantId?: string; // Related participant
[key: string]: unknown; // Extensible properties
}Trigger: When a new participant joins a split
Payload Schema:
interface ParticipantJoinedEvent {
splitId?: string; // Split ID (optional, set by emitter)
participantId: string; // Participant ID (required)
userId?: string; // User ID
joinedAt?: string; // ISO timestamp
amountOwed?: number; // Amount owed by participant
status?: string; // Participant status
timestamp?: string; // ISO timestamp
[key: string]: unknown; // Extensible properties
}Trigger: When a client broadcasts activity via split_activity event
Payload Schema: Same as SplitActivityData from client events
{
splitId: string;
activity: SplitActivityData;
}- Authentication Failure: Connection immediately terminated
- Invalid JWT Format: Connection immediately terminated
- Expired Token: Connection immediately terminated
- Validation Errors:
400 Bad Requestwith descriptive message - Authorization Errors:
401 Unauthorizedfor protected operations - Missing Required Fields:
400 Bad Requestspecifying missing field
Errors are handled by Socket.IO's built-in error mechanism and will trigger the client's error handlers.
- All client events require valid JWT authentication
- Room access is validated via
AuthorizationService.canAccessSplit() - JWT signature verification uses timing-safe comparison
- CORS is configured for cross-origin requests
- Room operations use Socket.IO's efficient adapter
- Broadcast operations are room-scoped to minimize traffic
- Connection validation happens once during handshake
- All event interfaces use
[key: string]: unknownfor future extensibility - Activity data is flexible to support various use cases
- Server events can include additional metadata as needed
The frontend should use this contract as the canonical reference for:
- WebSocket connection establishment
- Event payload structures
- Error handling expectations
- Room management patterns
For detailed request/response examples, see docs/ws-event-examples.md.