Signaling Protocol Reference
Overview
The SecureCall signaling server handles real-time communication between clients during call setup using WebSocket connections, and provides a simple HTTP endpoint for health monitoring. The server is deployed on Railway and acts as an opaque relay — it forwards encrypted signaling messages between clients but cannot read or modify their content.
The signaling server is responsible for:
- Client registration — mapping client IDs to WebSocket connections
- Call routing — forwarding call initiation, acceptance, and rejection messages
- ICE candidate exchange — relaying WebRTC ICE candidates for NAT traversal
- Public Key Directory (PKD) — storing and retrieving client public keys for E2E encryption
- Presence tracking — monitoring client online status via heartbeats
REST API
The signaling server exposes a single HTTP endpoint for health monitoring. This endpoint requires no authentication and is used by Railway for deployment health checks.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
None | Health check — returns {"status":"ok"} |
Example Request
GET /health HTTP/1.1
Host: protective-healing-production.up.railway.app
HTTP/1.1 200 OK
Content-Type: application/json
{"status":"ok"}
Connection
Clients connect to the signaling server via a secure WebSocket connection. All communication after the initial handshake uses JSON-encoded messages.
Connection URL
wss://protective-healing-production.up.railway.app/signal
Message Format
All WebSocket messages follow a consistent JSON structure with a type field identifying the message kind and a payload object containing message-specific data:
{
"type": "MESSAGE_TYPE",
"payload": { ... }
}
REGISTER message within 60 seconds to associate its client ID with the connection. Failure to register will result in the server closing the connection.
Typical Call Flow
A standard call setup follows this sequence:
- Both clients connect and send
REGISTERmessages - Client A sends
CALL_INITIATEwith an SDP offer targeting Client B - Server forwards the offer to Client B as
INCOMING_CALL - Client B sends
CALL_ACCEPTwith an SDP answer - Server forwards the answer to Client A as
CALL_ACCEPTED - Both clients exchange
ICE_CANDIDATEmessages via the server - WebRTC peer-to-peer connection is established — media flows directly
- Either client sends
CALL_ENDto terminate the call
Client → Server Messages
These message types are sent from the client application to the signaling server.
| Type | Description | Payload |
|---|---|---|
REGISTER |
Register client ID with server | { "clientId": "abc123" } |
CALL_INITIATE |
Start a call to another client | { "targetId": "xyz789", "sdpOffer": "..." } |
CALL_ACCEPT |
Accept an incoming call | { "callId": "...", "sdpAnswer": "..." } |
CALL_REJECT |
Reject an incoming call | { "callId": "..." } |
CALL_END |
End an active call | { "callId": "..." } |
ICE_CANDIDATE |
ICE candidate for NAT traversal | { "callId": "...", "candidate": "..." } |
PKD_REGISTER |
Register public key in directory | { "clientId": "...", "publicKey": "..." } |
PKD_LOOKUP |
Look up a client's public key | { "clientId": "..." } |
Server → Client Messages
These message types are sent from the signaling server to client applications, either as responses to client requests or as notifications of events initiated by other clients.
| Type | Description | Payload |
|---|---|---|
REGISTERED |
Registration confirmed | { "clientId": "abc123" } |
INCOMING_CALL |
Incoming call notification | { "callId": "...", "callerId": "...", "sdpOffer": "..." } |
CALL_ACCEPTED |
Call was accepted by peer | { "callId": "...", "sdpAnswer": "..." } |
CALL_REJECTED |
Call was rejected by peer | { "callId": "..." } |
CALL_ENDED |
Call ended by peer | { "callId": "..." } |
ICE_CANDIDATE |
ICE candidate from peer | { "callId": "...", "candidate": "..." } |
PKD_RESULT |
Public key lookup result | { "clientId": "...", "publicKey": "..." } |
ERROR |
Error message | { "code": "...", "message": "..." } |
Error Codes
The ERROR message type includes a code field that indicates the specific error condition:
INVALID_MESSAGE— Malformed JSON or missing required fieldsNOT_REGISTERED— Client attempted an operation before registeringTARGET_NOT_FOUND— The target client ID is not connectedCALL_NOT_FOUND— The specified call ID does not existRATE_LIMITED— Too many requests from this connection
Server Parameters
The signaling server enforces the following limits to prevent abuse and ensure stability. Clients that exceed these limits may have their connections terminated or receive ERROR messages with the RATE_LIMITED code.
| Parameter | Value |
|---|---|
| Max payload size | 64 KB |
| Max connections per IP | Rate-limited (enforced by server) |
| Heartbeat interval | 30 seconds |
| Connection timeout | 60 seconds (no heartbeat) |
| Client ID format | ^[a-zA-Z0-9_-]{1,64}$ |
HeartbeatClient) to maintain the connection. If the server does not receive any message for 60 seconds, it will close the WebSocket connection. The client implements exponential backoff reconnection (1s to 30s max) to handle disconnections gracefully.
Relay Authentication
TURN server credentials are provisioned by the signaling server during call setup. Rather than embedding static credentials in the client application, the server generates time-limited credentials using a shared secret with the TURN provider (Metered.ca).
This approach ensures that:
- TURN credentials rotate automatically and cannot be reused after expiration
- Compromised credentials have a limited window of abuse
- The TURN shared secret is stored only on the server (via environment variables), not in client code
TURN Server Configuration
| Parameter | Value |
|---|---|
| Provider | Metered.ca |
| Server | turn:a.relay.metered.ca:443?transport=tcp |
| Transport | TCP on port 443 (maximizes firewall compatibility) |
| Credential type | Time-limited (HMAC-based) |