The SubStream Protocol WebSocket API provides real-time event streaming for merchant dashboards. This API enables instant notifications for payment events, trial conversions, and payment failures.
All WebSocket connections must be authenticated using SEP-10 JSON Web Tokens (JWTs).
// Option 1: Using auth parameter
const socket = io('ws://localhost:3001/merchant', {
auth: {
token: 'your-sep-10-jwt-token'
}
});
// Option 2: Using Authorization header
const socket = io('ws://localhost:3001/merchant', {
extraHeaders: {
Authorization: 'Bearer your-sep-10-jwt-token'
}
});- Algorithm: RS256 or ES256
- Claims:
sub(required): Stellar public key of the merchantexp(required): Token expiration timestampiat(optional): Token issued at timestamp
The server validates tokens by:
- Verifying cryptographic signature
- Checking token expiration
- Extracting Stellar public key from
subclaim - Assigning connection to merchant-specific room
ws://localhost:3001/merchant
| Variable | Description | Default |
|---|---|---|
WS_PORT |
WebSocket server port | 3001 |
JWT_SECRET |
JWT verification secret | Required |
CORS_ORIGIN |
CORS allowed origins | * |
REDIS_PUBSUB_URL |
Redis Pub/Sub connection | redis://localhost:6379 |
Emitted when a successful pull payment is processed.
interface PaymentSuccessPayload {
stellarPublicKey: string;
planId: string;
amount: string;
timestamp: string;
transactionHash: string;
}socket.on('payment_success', (data) => {
console.log('Payment received:', data);
// data.type: 'payment_success'
// data.data: PaymentSuccessPayload
// data.timestamp: ISO string
});Emitted when a payment fails. Supports batching for high-volume failures.
interface PaymentFailedPayload {
stellarPublicKey: string;
planId: string;
userId: string;
failureReason: string;
timestamp: string;
deepLinkRef: string;
}interface BatchedPaymentFailedPayload {
stellarPublicKey: string;
failures: PaymentFailedPayload[];
batchId: string;
timestamp: string;
totalCount: number;
}| Code | Description |
|---|---|
INSUFFICIENT_FUNDS |
Account lacks sufficient balance |
ACCOUNT_FROZEN |
Account is frozen |
ACCOUNT_SUSPENDED |
Account is suspended |
INVALID_SIGNATURE |
Transaction signature is invalid |
NETWORK_ERROR |
Network connectivity issue |
TIMEOUT |
Transaction timed out |
RATE_LIMITED |
Rate limit exceeded |
socket.on('payment_failed', (data) => {
if (data.data.failures) {
// Batched failures
console.log(`${data.data.totalCount} payment failures:`, data.data.failures);
} else {
// Individual failure
console.log('Payment failed:', data.data);
}
});Emitted when a trial user converts to a paid plan.
interface TrialConvertedPayload {
stellarPublicKey: string;
planId: string;
userId: string;
timestamp: string;
}socket.on('trial_converted', (data) => {
console.log('Trial converted:', data);
});The server sends periodic ping messages to detect zombie connections.
socket.on('ping', (data) => {
console.log('Ping received:', data.timestamp);
// Server automatically handles pong response
});socket.on('connected', (data) => {
console.log('Connected to SubStream Protocol');
console.log('Merchant ID:', data.merchantId);
});socket.on('error', (error) => {
console.error('WebSocket error:', error.message);
});socket.on('token_expired', (data) => {
console.log('Token expired:', data.message);
// Client should reconnect with fresh token
});socket.on('timeout', (data) => {
console.log('Connection timeout:', data.message);
// Client should reconnect
});Each merchant is assigned to a dedicated room named after their Stellar public key. This ensures:
- Cross-tenant data leakage prevention: Merchants only receive their own events
- Mathematical isolation: Room assignments are cryptographically bound to public keys
- No broadcast overlap: Events are routed only to intended recipients
- Active token validation on long-lived connections
- Automatic disconnection when tokens expire
- Real-time token expiration checks during heartbeat
- Connection timeout: 5 minutes of inactivity
- Heartbeat interval: 30 seconds
- Automatic cleanup: Zombie connections are purged
The WebSocket gateway uses Redis Pub/Sub for horizontal scaling:
- Event Publishing: Any pod can publish events to Redis channels
- Universal Subscription: All WebSocket pods subscribe to Redis channels
- Cross-pod Communication: Events are distributed across all instances
- Load Distribution: Clients can connect to any available pod
| Channel | Purpose | Payload |
|---|---|---|
payment_success |
Successful payments | PaymentSuccessPayload |
payment_failed |
Failed payments | PaymentFailedPayload |
trial_converted |
Trial conversions | TrialConvertedPayload |
- Reconnection: Automatic Redis reconnection with exponential backoff
- Buffering: Events are buffered during Redis outages
- Graceful degradation: Service continues with local events during Redis failures
import { io, Socket } from 'socket.io-client';
class SubStreamWebSocket {
private socket: Socket;
private token: string;
constructor(token: string) {
this.token = token;
this.socket = io('ws://localhost:3001/merchant', {
auth: { token }
});
this.setupEventHandlers();
}
private setupEventHandlers() {
this.socket.on('connected', (data) => {
console.log('Connected:', data);
});
this.socket.on('payment_success', (data) => {
this.handlePaymentSuccess(data);
});
this.socket.on('payment_failed', (data) => {
this.handlePaymentFailure(data);
});
this.socket.on('trial_converted', (data) => {
this.handleTrialConversion(data);
});
this.socket.on('error', (error) => {
console.error('WebSocket error:', error);
});
this.socket.on('token_expired', () => {
this.reconnect();
});
}
private handlePaymentSuccess(data: any) {
// Update UI with successful payment
console.log('Payment success:', data);
}
private handlePaymentFailure(data: any) {
// Show payment failure notification
console.log('Payment failure:', data);
}
private handleTrialConversion(data: any) {
// Update trial conversion metrics
console.log('Trial conversion:', data);
}
private reconnect() {
// Implement token refresh and reconnection logic
const newToken = this.refreshToken();
this.socket = io('ws://localhost:3001/merchant', {
auth: { token: newToken }
});
}
disconnect() {
this.socket.disconnect();
}
}
// Usage
const client = new SubStreamWebSocket('your-sep-10-jwt-token');The WebSocket API includes comprehensive integration tests covering:
- Authentication scenarios
- Real-time event delivery
- Cross-tenant isolation
- Redis scaling
- Error handling
Run tests with:
npm run test:wsSecurity tests verify:
- JWT token validation
- Cross-tenant data leakage prevention
- Token expiration handling
- Unauthorized connection rejection
| Error | Description | Action |
|---|---|---|
AUTHENTICATION_FAILED |
Invalid or missing token | Provide valid SEP-10 JWT |
TOKEN_EXPIRED |
Authentication token expired | Refresh token and reconnect |
CONNECTION_TIMEOUT |
Inactivity timeout | Reconnect to server |
REDIS_ERROR |
Redis connectivity issue | Service continues with local events |
- Active connections per merchant
- Connection duration statistics
- Token expiration events
- Error rates by type
- Events per second (EPS)
- Event delivery latency
- Batch processing statistics
- Redis queue depth
curl http://localhost:3001/healthReturns WebSocket gateway health status including Redis connectivity.
apiVersion: apps/v1
kind: Deployment
metadata:
name: websocket-gateway
spec:
replicas: 3
selector:
matchLabels:
app: websocket-gateway
template:
metadata:
labels:
app: websocket-gateway
spec:
containers:
- name: websocket-gateway
image: substream/websocket-gateway:latest
ports:
- containerPort: 3001
env:
- name: REDIS_PUBSUB_URL
value: "redis://redis-cluster:6379"
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: jwt-secret
key: secret- Dedicated Instance: Separate Redis cluster for Pub/Sub
- Memory: 2GB minimum for production
- Persistence: Disabled for Pub/Sub (memory-only)
- Clustering: Enabled for horizontal scaling
- Per Merchant: 100 concurrent connections
- Per IP: 50 concurrent connections
- Global: 10,000 concurrent connections
- Payment Events: 1000 events/second per merchant
- Batch Processing: 10 events/second threshold
- Burst Capacity: 5000 events/second for 1 minute
-
Connection Rejected
- Check JWT token validity
- Verify
subclaim contains Stellar public key - Ensure token is not expired
-
Missing Events
- Verify Redis connectivity
- Check merchant room assignment
- Review event payload format
-
Performance Issues
- Monitor Redis memory usage
- Check connection pool size
- Review batch processing metrics
Enable debug logging:
DEBUG=socket.io:* npm run start:ws:dev- Initial WebSocket gateway implementation
- SEP-10 JWT authentication
- Redis Pub/Sub scaling
- Real-time payment events
- Dunning alert system
- Comprehensive test suite