Skip to content

esu — Realtime ​

@playpals/esu · port 4010 · apps/esu

Esu governs language, mediation, and dynamic exchange — the metaphor for a socket server relaying real-time communication between clients.

A Socket.IO server that holds connections and fans out events. It persists nothing and authorizes nothing on its own. Two replicas in production, coordinated through the Redis adapter.

Running it ​

bash
pnpm --filter @playpals/esu dev
curl http://localhost:4010/health          # {"ok":true}

JWT_SECRET must match the API's, or every connection is rejected.

What it does ​

ResponsibilityDetail
Authenticate socketsVerifies the JWT from socket.handshake.auth.token
Room membershipuser:, dm:, room:, channel: rooms
PresenceIn-memory map of user → socket ids
Typing indicatorsEphemeral, never persisted
Interservice fan-outPOST /admin/emit/*, called by api and ogun

What it deliberately does not do ​

  • Write to the database. Messages are POSTed to the API, which persists and then asks esu to emit. This is why esu can restart mid-conversation with no data loss.
  • Authorize joins. It calls back into the API on every dm:join and room:join.

That separation is the main design decision in this service. Preserve it.

Source layout ​

src/
├── index.ts                    dotenv + start()
├── http/server.ts              HTTP server, io setup, /health, /admin mount
├── http/app.ts                 Express app
├── config/index.ts             typed env config
├── auth/
│   ├── jwt.ts                  token verification
│   ├── socketAuth.ts           handshake authentication
│   └── apiAuthz.ts             callbacks to the API for join authorization
├── socket/
│   ├── io.ts                   Socket.IO server, Redis adapter, Admin UI
│   └── handlers.ts             every client event handler
├── interservice/
│   ├── adminRoutes.ts          POST /admin/emit/*
│   └── adminAuth.ts            X-Realtime-Admin-Token guard
├── presence/status.ts          online = any active socket, via fetchSockets()
├── rooms/roomNames.ts          room key builders
└── lib/                        logger, socket event logging

Client events handled ​

EventBehaviour
pingAcks with { ts }
dm:joinAuthorizes via the API, then joins dm:<id>; acks AUTHZ_DENIED on refusal
dm:leaveLeaves the room
dm:typing:start / dm:typing:stopBroadcasts dm:typing to others in the room
room:joinAuthorizes via the API, then joins room:<id>
room:leaveLeaves the room
room:typing:start / room:typing:stopBroadcasts room:typing
presence:state{ active } — app foregrounded or not; republishes the user's status
presence:subscribeAuthorizes via the API, joins presence:<id> per allowed user, acks { ok, statuses }
presence:unsubscribeLeaves presence:<id> rooms
disconnectRepublishes the user's status (offline once no active socket remains)

Full contract and the known drift between it and packages/types: Socket Events.

Interservice admin API ​

Mounted at /admin, guarded by requireAdminToken (src/interservice/adminAuth.ts):

  • The API and ogun call esu on its cluster Service (http://playpalz-esu:4010). Anything that came through the public ingress carries X-Forwarded-For, so such a request gets a 404 before the token is even read.
  • X-Realtime-Admin-Token must match REALTIME_ADMIN_TOKEN, compared in constant time.

Tests: pnpm test in apps/esu (Node's test runner through tsx).

EndpointEmitsTo room
POST /admin/emit/dm/message/newdm:message:newdm:<conversationId>
POST /admin/emit/dm/readdm:readdm:<conversationId>
POST /admin/emit/room/message/newroom:message:newroom:<roomId>
POST /admin/emit/channel/room/createdchannel:room:createdchannel:<channelId>
POST /admin/emit/notification/newnotification:newuser:<userId>
POST /admin/emit/user/disconnect(disconnects every socket)user:<userId>
POST /admin/emit/media/processing/updatemedia:processing:updateuser:<userId>

One query endpoint sits alongside them: POST /admin/dm/active-viewers returns which of userIds are in dm:<conversationId>. The API uses it to skip DM notifications for people already viewing the thread.

Scaling ​

The Redis adapter (@socket.io/redis-adapter) makes an emit on any pod reach sockets on every pod:

ts
const pub = new Redis(port, host, { maxRetriesPerRequest: null, enableReadyCheck: true, password });
const sub = pub.duplicate();
await Promise.all([onceReady(pub), onceReady(sub)]);
io.adapter(createAdapter(pub, sub));

Startup blocks until both clients are ready, so the server never accepts a connection it cannot fan out from. SIGTERM and SIGINT quit both clients cleanly.

Both transports stay enabled:

ts
transports: ["websocket", "polling"]

Polling is required by the Socket.IO Admin UI's initial handshake, and polling requests for one client must land on one pod — hence the ESUSESSION sticky cookie on the socket ingress.

Presence is derived from live sockets through the adapter's fetchSockets(), so it is consistent across replicas. See Realtime.

Socket.IO Admin UI ​

A live dashboard of connections, rooms, and events, disabled by default:

bash
pnpm --filter @playpals/esu admin-ui:creds     # generates a bcrypt hash
bash
ADMIN_UI_ENABLED=true
ADMIN_UI_USERNAME=admin
ADMIN_UI_PASSWORD_HASH=<hash>

Then open admin.socket.io and point it at the server. Enabling it also adds https://admin.socket.io to the CORS allowlist. It is instrumented in production mode, so event payloads are not exposed.

Configuration ​

VariableDefaultNotes
PORT4010
CORS_ORIGIN*Comma-separated list
JWT_SECRET""Must match the API
JWT_ALGHS256
API_BASE_URLhttp://localhost:3000/api/v1Authorization callbacks — set to :4000 locally
API_SERVICE_TOKEN""Sent as X-Service-Token on callbacks
REALTIME_ADMIN_TOKEN""Guards /admin
REDIS_URL / REDIS_HOST / REDIS_PORT / REDIS_PASSWORDlocalhostAdapter connection
SOCKET_EVENT_LOGGINGtrueLogs every inbound and outbound event
ADMIN_UI_ENABLEDfalse
ADMIN_UI_USERNAME / ADMIN_UI_PASSWORD_HASH—Required when the UI is enabled

The default API_BASE_URL port is wrong for local development

.env.example ships http://localhost:3000/api/v1, but the API's own .env.example sets PORT=4000. Left as shipped, every room join is denied. Set API_BASE_URL=http://localhost:4000/api/v1.

Deployment ​

PropertyValue
Helm valuesinfra/helm/values/production/esu.yaml (release playpalz-esu)
Replicas2, spread across nodes
ProbesLiveness and readiness on /health
Ingresssocket.playpalz.gg, its own Ingress object
Ingress notesproxy-http-version: 1.1, 3600 s timeouts, ESUSESSION sticky cookie
bash
./scripts/build-esu.sh [tag]

The socket ingress is separate from the REST ingress because its annotations must not apply to REST routes.

Debugging ​

bash
kubectl logs -l app=playpalz-esu --tail=100 -f

With SOCKET_EVENT_LOGGING=true, every event is logged as socket:in / socket:out with the event name, user id, and socket id. In Grafana:

{app="playpalz-esu"} | json | event = "socket:in"

Common failures: a JWT_SECRET mismatch (connect-then-drop), a wrong API_BASE_URL (every join denied), and Redis auth failures (fan-out silently stops).

Internal documentation — PlayPalz platform