Skip to main content

DEJA Server

The DEJA Server is the backend bridge between your browser-based frontend apps and the DCC-EX CommandStation hardware. Written in TypeScript and running on Node.js, it listens for commands from Firebase, translates them into DCC-EX serial protocol, and sends them over USB to the CommandStation. It also provides a WebSocket server for real-time bidirectional communication with connected clients.

Server architecture diagram

Three Subsystems

The server is composed of three independently togglable subsystems, each controlled by an environment variable. You can run any combination depending on your setup.

Firebase Cloud (ENABLE_DEJACLOUD)

When enabled, the server connects to Firebase (both Firestore and Realtime Database) and listens for layout changes. This is the primary command pipeline:

  • RTDB listeners on dccCommands/{layoutId} pick up DCC commands pushed by frontend apps and dispatch them to the DCC handler.
  • RTDB listeners on dejaCommands/{layoutId} handle system commands (device connect, port listing, status queries) that never touch the serial port.
  • Firestore snapshot listeners on layouts/{layoutId}/throttles, turnouts, effects, and signals detect state changes and translate them into serial commands.

On startup, the server initializes the layout and begins listening. On shutdown, it cleans up all listeners, resets device connection states, and zeros out throttle speeds.

MQTT (ENABLE_MQTT)

When enabled, the server connects to an MQTT broker (Mosquitto by default). This layer is used for communication with IoT devices such as Arduino and Pico W boards running DEJA firmware. MQTT topics follow the pattern {topicId}/{layoutId}/{deviceId}.

WebSocket (ENABLE_WS)

Enabled by default. The server starts a WebSocket server on the port specified by VITE_WS_PORT (default: 8082). This provides:

  • Broadcast messaging -- DCC commands, connection status, port lists, and status updates are sent to all connected browser clients.
  • Device-specific serial monitoring -- Clients can subscribe to individual devices to receive filtered serial I/O for that device only.

On client connection, the server sends an acknowledgment message with the layout ID and server ID, followed by a connection notification with the client's IP address.

Entry Point

The server entry point is index.ts at the root of apps/server/. It initializes Sentry for error tracking, reads the environment toggle flags, and starts each enabled subsystem in sequence:

  1. DEJA Cloud (Firebase listeners and layout initialization)
  2. MQTT (broker connection)
  3. WebSocket (server start)

The server registers handlers for SIGINT and SIGTERM to perform a clean shutdown, and catches uncaught exceptions and unhandled promise rejections to log fatal errors before exiting.

Command Flow

The full path from a user action in the browser to a DCC signal on the track:

Frontend (Vue app)
  |
  |  useDcc().setFunction(addr, func, state)
  |
  v
Firebase RTDB: dccCommands/{layoutId}
  |
  v
Server: dejaCloud.ts -- rtdb.ref.on('child_added')
  |
  v
dcc.ts: handleDccChange() -> handleMessage()
  |
  |-- 'throttle'  -> sendSpeed()    -> send('t addr speed dir')
  |-- 'turnout'   -> sendTurnout()  -> send('T idx state')
  |-- 'function'  -> sendFunction() -> send('F addr func state')
  |-- 'output'    -> sendOutput()   -> send('Z pin state')
  |-- 'dcc'       -> send()         -> raw command passthrough
  |-- 'power'     -> power()        -> send('1' or '0')
  |
  v
serial.send(port, '<cmd>\n')
  |
  v
DCC-EX CommandStation (115200 baud)

Each command is validated before being sent. The validateDccCommand function rejects strings longer than 200 characters or containing protocol-breaking characters (angle brackets, newlines). Numeric parameters are checked with isFiniteNumber to prevent NaN or Infinity from reaching the hardware.

After a command is processed from RTDB, the server removes it from the queue to prevent re-processing.

WebSocket Protocol

Messages are JSON objects with an action field and a payload field.

Broadcast Actions

ActionPayloadDescription
ack{ layoutId, serverId }Sent on initial client connection
wsconnected{ ip, serverId }Client IP and server identification
dccCommand stringDCC command broadcast to all clients
connected{ baudRate, device, path }Serial port connected
portListArray of port pathsAvailable serial ports
status{ client, isConnected }Server connection status

Device Subscription

Clients can subscribe to device-specific serial monitoring:

  • Send { action: "subscribe-device", deviceId: "..." } to start receiving serial data for a device.
  • Send { action: "unsubscribe-device", deviceId: "..." } to stop.
  • The server confirms with device-subscribed or device-unsubscribed responses.
  • Serial data arrives as { action: "serial-data", payload: { deviceId, data, direction, timestamp } }.

See the WebSocket Protocol page for the full specification.

Serial Communication

The server communicates with the DCC-EX CommandStation at 115200 baud over USB serial. Every DCC-EX command is wrapped in angle brackets and terminated with a newline: <command>\n. The send() function in dcc.ts handles this wrapping automatically -- callers pass only the inner command string.

Power commands (1, 0, 1 MAIN, 0 MAIN, 1 PROG, 0 PROG) are detected and optimistically written to Firestore so that frontend apps can reflect the power state immediately.

Source Structure

apps/server/
|-- index.ts                      Entry point
|-- src/
    |-- dejaCloud.ts              Firebase listeners and lifecycle
    |-- broadcast.ts              Broadcast to WebSocket clients
    |-- lib/
    |   |-- dcc.ts                DCC command handler
    |   |-- deja.ts               DEJA system command handler
    |   |-- mqtt.ts               MQTT client
    |   |-- serial.ts             SerialPort wrapper
    |   |-- sound.ts              Sound playback via play-sound
    |   |-- ws-server.ts          WebSocket server
    |   |-- AudioCacheService.ts  Audio file caching
    |-- modules/
    |   |-- effects.ts            Effect handling (sounds, lights)
    |   |-- layout.ts             Layout initialization
    |   |-- sensors.ts            Sensor data handling
    |   |-- signals.ts            Signal state handling
    |   |-- throttles.ts          Throttle command handling
    |   |-- turnouts.ts           Turnout command handling
    |-- utils/
        |-- logger.ts             Signale-based logger

Environment Variables

VariableDefaultDescription
LAYOUT_IDbetatrackLayout identifier in Firebase
ENABLE_DEJACLOUDfalseEnable Firebase Cloud integration
ENABLE_MQTTfalseEnable MQTT broker connection
ENABLE_WStrueEnable WebSocket server
VITE_WS_PORT8082WebSocket server port
VITE_WS_IDDEJA.jsServer identifier string
SENTRY_DSN--Sentry error tracking DSN

Running the Server

Development

Start the server in watch mode using tsx:

pnpm --filter=deja-serverts dev

This runs tsx watch index.ts, which automatically restarts the server when source files change.

Production

Use pm2 to run the server as a managed process:

pm2 start bash --name deja-start -- -lc "pnpm turbo run start --filter=apps/server --filter=apps/monitor"
pm2 save
pm2 startup

This starts both the server and the Monitor app together under pm2, with automatic restart on boot.

Logging

The server uses signale for structured logging. The logger utility in src/utils/logger.ts exports a log object with methods for different log levels:

  • log.start() -- Process startup messages
  • log.success() -- Successful operations
  • log.error() -- Error conditions
  • log.fatal() -- Fatal errors before exit
  • log.note() -- Informational notes
  • log.star() -- Highlighted command logs
  • log.info() -- General information
  • log.warn() -- Warning conditions