Architecture
Architecture
Overview
IOServer is a thin orchestration layer on top of two established libraries:
- Fastify — HTTP server, route registration, plugin ecosystem
- Socket.IO — WebSocket server with namespace, room, and event abstractions
Both run inside the same Node.js process and share the same underlying TCP server. Socket.IO attaches to the Fastify HTTP server once webapp.ready() resolves.
┌────────────────────────────────────────────────────────────────┐
│ Node.js process │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IOServer │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌───────────────────────────┐ │ │
│ │ │ Fastify (HTTP) │ │ Socket.IO (WebSocket) │ │ │
│ │ │ │ │ │ │ │
│ │ │ Controllers │ │ Services │ │ │
│ │ │ Middlewares │ │ Middlewares │ │ │
│ │ └──────────────────┘ └───────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ AppHandle │ │ │
│ │ │ send() log() verbose [managers...] │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ Managers │ │ Watchers │ │ │
│ │ │ (singleton) │ │ (background loops) │ │ │
│ │ └──────────────┘ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
▲ HTTP ▲ WebSocket
│ │
REST clients Browser / Node clients
Startup sequence
Understanding the initialization order prevents subtle bugs with component access.
new IOServer(options)
↓
1. Validate port and log level
2. Initialize Fastify
3. Register plugins (CORS, sensible, optional @fastify/static)
4. Attach Socket.IO after webapp.ready()
server.addManager(...) ← instantiated IMMEDIATELY, added to appHandle
server.addWatcher(...) ← instantiated, watch() deferred to start()
server.addService(...) ← instantiated, namespace setup deferred to start()
server.addController(...) ← instantiated, routes registered NOW in Fastify
server.start()
↓
5. Wait for Socket.IO to be ready (polls up to 1 second)
6. Create Socket.IO namespaces for all services
7. Apply service middlewares to namespaces
8. Register connection handlers (public methods → socket.on())
9. Call watch() on all watchers (non-blocking, errors logged)
10. webapp.listen()
this.appHandle.managerName. They are the only component type instantiated synchronously at registration time.The AppHandle
Every component receives an appHandle as its constructor argument and stores it as protected appHandle. It is the sole communication channel between components.
interface AppHandle {
send: (options: SendToOptions) => boolean; // Push WS event to clients
log: (level: number, text: string) => void; // Leveled logging
verbose: LogLevel; // Current log level
[key: string]: any; // Registered managers by name
}
When addManager({ name: "db", manager: DatabaseManager }) is called, appHandle.db is set to the DatabaseManager instance. Any component registered afterwards can call this.appHandle.db.findUser(id).
Error propagation
HTTP errors (Controllers)
Fastify's error handler is configured to serialise IOServerError instances to a structured JSON response:
{
"statusCode": 404,
"error": "IOServerError",
"message": "User not found"
}
Throw new IOServerError(message, statusCode) from any controller method; the framework sends the correct HTTP response automatically.
WebSocket errors (Services)
When a service method throws, IOServer catches it and sends the error back:
- If the original call had a
callback→ callscallback({ status: "error", type, message, statusCode }) - Otherwise →
socket.emit("error", { status: "error", type, message, statusCode })
The client always receives structured error data regardless of whether it used callback-style or event-based invocation.
Static file serving (SPA mode)
When rootDir is set and the directory exists, IOServer registers @fastify/static at prefix /. Registered API routes always take priority.
When spaFallback: true (default), any request that matches no API route returns index.html — enabling client-side routers (Vue Router, React Router, etc.).
const server = new IOServer({
rootDir: path.join(__dirname, "../frontend/dist"),
spaFallback: true, // default
});