A Redis-only, stateless ChatService secured via JWTs issued by the MainServer. Supports both 1:1 and group chats, sliding inactivity TTL, secure WebSocket connections, and token refresh.
2.1 App JWT (Issued at login)
- Contains:
userId,role, broad app permissions - Long expiry (e.g., 30 days)
2.2 Chat JWT (Issued on session creation/join)
-
Contains:
userId- Optional
anonymousId(for privacy) chatPermissions:["send","read"]exp: session lifetime (e.g., 7 days)
-
Signed with shared secret known to both services
-
Stateless: ChatService only does
jwt.verify(token, SECRET)
2.3 Minimal-Claims Approach
-
JWT carries identity, not full room membership
-
On WebSocket handshake, ChatService:
- Verifies JWT signature &
exp - Extracts
userId - Reads Redis metadata for
sessionId - Checks
userId∈ participants list
- Verifies JWT signature &
For each chat session chat_{id}:
-
chat_{id}:metadata(HASH)participants: JSON array of user IDscreatedAt: timestampstatus:"active"
-
chat_{id}:messages(LIST)- JSON-encoded messages
- Only last N messages (list-trimmed)
-
user_{id}:sessions(SET)- Tracks all session IDs a user can access
Messages keep the session alive; idle sessions auto-expire.
async function onNewMessage(sessionId, msg) {
const metaKey = `${sessionId}:metadata`;
const msgsKey = `${sessionId}:messages`;
const TTL = 7*24*3600; // 7 days
await redis.rpush(msgsKey, JSON.stringify(msg));
await redis.ltrim(msgsKey, -N, -1);
await redis.expire(metaKey, TTL);
await redis.expire(msgsKey, TTL);
}- First message sets TTL
- Each message resets TTL to 7 days
- If no message in 7 days → Redis auto-deletes keys
-
Client: opens WebSocket to
wss://chatservice/session/{sessionId}with headerAuthorization: Bearer <Chat-JWT> -
ChatService handshake:
- Verifies token
- Extracts
userId - Looks up metadata:
redis.hget(sessionId:metadata, "participants") - Confirms membership
-
On message:
onNewMessage()pushes to Redis list + TTL resetredis.publish(sessionId, JSON.stringify(msg))- ChatService broadcasts to all connected sockets for that session
-
Offline: if user disconnected, ChatService can trigger FCM push
Endpoint: POST /api/chat/refresh
- Headers:
Authorization: Bearer <App-JWT> - Body:
{ sessionId, chatToken }
MainServer:
- Verifies App-JWT
- Verifies Chat-JWT signature (ignore
exp) → extractsuserId - Checks
redis.exists(sessionId:metadata) - Checks
userId∈ participants - Issues new Chat-JWT with fresh
exp
Client:
- Parses old JWT
exp - Fires refresh when
now + buffer >= exp - Replaces stored token seamlessly
Frontend calls GET /api/chat/sessions:
- MainServer reads from
user_{userId}:sessions(Redis SET) - Returns list of session IDs, metadata (e.g., names, lastMessageAt)
- Client uses these IDs to display chat list
-
Create session:
const sessionId = "chat_" + uuidv4(); const participants = [...]; redis.hset(`${sessionId}:metadata`, { participants: JSON.stringify(participants), createdAt: Date.now(), status: "active" }); redis.expire(`${sessionId}:metadata`, TTL); redis.sadd(`user_${userId}:sessions`, sessionId); participants.forEach(u => redis.sadd(`user_${u}:sessions`, sessionId));
-
JWT doesn’t need full participant list
-
Access check always hits Redis once per handshake
- One Redis lookup per WebSocket handshake → ~1ms
- No per-message auth calls once socket open
- Sliding TTL auto-cleans idle data
- SETs track user ↔ session mappings
- LIST-trim keeps memory bounded
This design keeps ChatService stateless (aside from Redis), secures each connection via JWT+Redis check, auto-expires idle sessions, supports DM & group chats, and allows clean token refresh—all with Redis only.