Concepts

Architecture

How IOServer combines Fastify and Socket.IO, and how components communicate.

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()
Managers must be registered before any service, controller, or watcher that references them via 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 → calls callback({ 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
});
Copyright © 2026