Realtime messaging and presence server over native WebSocket.
Part of the Symple ecosystem:
- symple-client - JavaScript client (browser & Node.js)
- symple-client-ruby - Ruby/Rails server-side emitter
- Presence - peer online/offline status broadcasting
- Scoped messaging - direct (peer-to-peer), user-level, room-level, or global broadcast
- Team/group permissions - peers only see and message others in their assigned rooms
- Dynamic rooms - clients can optionally join and leave rooms at runtime
- Token authentication - session validation via Redis (or anonymous mode)
- Server-side emission - push messages from Ruby/Rails via Redis pub/sub
- SSL/TLS - optional HTTPS/WSS support
git clone https://github.com/sourcey/symple-server.git
cd symple-server
npm install
npm startThe server listens on port 4500 by default. No Redis required - it runs in single-instance mode out of the box.
All configuration is via environment variables (loaded from .env via dotenv). Copy .env.example to .env to get started.
| Variable | Default | Description |
|---|---|---|
PORT |
4500 |
Port to listen on (also SYMPLE_PORT) |
SYMPLE_SESSION_TTL |
-1 |
Session TTL in minutes (-1 = no expiry) |
SYMPLE_AUTHENTICATION |
false |
Require token auth (needs Redis) |
SYMPLE_DYNAMIC_ROOMS |
true |
Allow clients to join/leave rooms |
SYMPLE_REDIS_URL |
- | Redis connection URL (enables Redis features) |
SYMPLE_REDIS_HOST |
- | Redis host (alternative to URL) |
SYMPLE_REDIS_PORT |
- | Redis port (alternative to URL) |
SYMPLE_SSL_ENABLED |
false |
Enable HTTPS/WSS |
SYMPLE_SSL_KEY |
- | Path to SSL key file |
SYMPLE_SSL_CERT |
- | Path to SSL certificate file |
Redis is optional. Without it, the server runs in single-instance mode with in-memory state. Set SYMPLE_REDIS_URL to enable:
- Token authentication - session lookup at
symple:session:<token> - Server-side emission - push messages from Ruby/Rails via symple-client-ruby
When SYMPLE_AUTHENTICATION=true, clients must provide user and token in the auth message (the first WebSocket message after connection). The server looks up the session in Redis at symple:session:<token> and merges it with the auth data.
When SYMPLE_AUTHENTICATION=false (default), clients only need to provide user.
Rooms are the permission boundary. A peer can only see presence from and send direct messages to peers that share at least one room. Every peer is auto-joined to their own user room on authentication.
There are three ways to assign rooms:
When your backend creates a session, include a rooms array. The server auto-joins the peer on authentication:
# Rails: on login, create a Symple session with team memberships
Symple.session.set(api_token.token, {
user: user.username,
rooms: user.teams.pluck(:slug) # ["team-a", "design", "project-42"]
}, ttl: 1.week.to_i)The client connects with the token; the server looks up the session, finds the rooms, and auto-joins:
const client = new SympleClient({
url: 'wss://your-server.com',
token: 'the-api-token',
peer: { user: 'alice', name: 'Alice' }
})The client can include rooms directly in the auth message. This only works when SYMPLE_AUTHENTICATION=false (no token validation), so the client is trusted:
// Client sends: { type: "auth", user: "alice", rooms: ["lobby", "vip"] }With SYMPLE_DYNAMIC_ROOMS=true (default), clients can join and leave rooms freely after authentication. This gives no permission scoping; any peer can join any room. Set SYMPLE_DYNAMIC_ROOMS=false to lock rooms to server-assigned only.
| Scenario | Behavior |
|---|---|
Alice in ["team-a"], Bob in ["team-a"] |
Can see each other's presence, can DM |
Alice in ["team-a"], Bob in ["team-b"] |
Invisible to each other, DMs blocked |
Alice in ["team-a", "design"], Bob in ["design"] |
Can see each other via design room |
Broadcast to room "team-a" |
Only reaches peers in team-a |
The welcome message includes the full room list so the client knows its memberships:
{ "type": "welcome", "protocol": "symple/4", "status": 200, "peer": {...}, "rooms": ["alice", "team-a", "design"] }Messages are routed based on the to field:
to value |
Behavior |
|---|---|
| Undefined | Broadcast to all sender's rooms (excluding sender) |
"user|id" |
Direct message to a specific peer (must share a room) |
"user" |
Broadcast to the user's room |
["room1", "room2"] |
Broadcast to multiple rooms |
const Symple = require('./lib/symple');
const { createConfig } = require('./config');
const config = createConfig();
const server = new Symple(config);
// Custom authentication with room assignment (supports async)
server.authenticate = async (peer, auth) => {
const user = await db.users.findByToken(auth.token);
if (!user) return { allowed: false };
return {
allowed: true,
rooms: user.teams // ["team-a", "design"]
};
};
// Post-auth hook (peer is already registered)
server.onAuthorize = function(ws, peer) {
console.log('Peer connected:', peer.name);
};
server.init();A C++ implementation with the same protocol is available in libsourcey.
Enable debug output with:
DEBUG=symple:* npm startFor more details, visit sourcey.com/code/symple.
MIT