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.

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, andsignalsdetect 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:
- DEJA Cloud (Firebase listeners and layout initialization)
- MQTT (broker connection)
- 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
| Action | Payload | Description |
|---|---|---|
ack | { layoutId, serverId } | Sent on initial client connection |
wsconnected | { ip, serverId } | Client IP and server identification |
dcc | Command string | DCC command broadcast to all clients |
connected | { baudRate, device, path } | Serial port connected |
portList | Array of port paths | Available 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-subscribedordevice-unsubscribedresponses. - 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
| Variable | Default | Description |
|---|---|---|
LAYOUT_ID | betatrack | Layout identifier in Firebase |
ENABLE_DEJACLOUD | false | Enable Firebase Cloud integration |
ENABLE_MQTT | false | Enable MQTT broker connection |
ENABLE_WS | true | Enable WebSocket server |
VITE_WS_PORT | 8082 | WebSocket server port |
VITE_WS_ID | DEJA.js | Server 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 messageslog.success()-- Successful operationslog.error()-- Error conditionslog.fatal()-- Fatal errors before exitlog.note()-- Informational noteslog.star()-- Highlighted command logslog.info()-- General informationlog.warn()-- Warning conditions