Backend Architecture

February 10, 2026 · View on GitHub

Intended audience: OSS developers working on the server.

This document does not cover: Frontend implementation or deployment.


Technology Stack

Framework: Fastify Language: TypeScript (compiled to JavaScript ES modules) Runtime: Node.js 18+ Package manager: npm

Rationale:

  • Fastify: Fast, low-overhead HTTP framework with schema validation
  • TypeScript: Type safety for config structures
  • ES modules: Modern JavaScript, tree-shaking, better performance

Directory Structure

server/
├── package.json           # Dependencies, scripts
├── tsconfig.json          # TypeScript config (ES modules)
├── jest.config.js         # Test configuration

├── data/                  # Runtime data directory
│   ├── configs/           # Preset library (*.json)
│   └── latest_dsp_state.json  # Recovery cache

└── src/
    ├── index.ts           # Entry point
    ├── app.ts             # Fastify app setup
    ├── logger.ts          # Pino logger
    ├── configPaths.ts     # Path resolution

    ├── routes/            # HTTP endpoint handlers
    │   ├── health.ts      # GET /health
    │   ├── version.ts     # GET /api/version
    │   ├── config.ts      # GET/PUT /api/state/latest
    │   └── configs.ts     # GET/PUT /api/configs/*

    ├── services/          # Business logic
    │   ├── configStore.ts      # Single-file persistence
    │   ├── configsLibrary.ts   # Preset library management
    │   └── shellExec.ts        # Safe shell execution (unused in prod)

    ├── types/
    │   └── errors.ts      # AppError class, error codes

    └── __tests__/         # Integration tests
        └── routes.test.ts

Application Lifecycle

Startup (server/src/index.ts)

1. Resolve data directory paths (configPaths.ts)
2. Create Fastify app (app.ts)
3. Register routes
4. Start HTTP server (port 3000 default)
5. Log "Server listening on http://0.0.0.0:3000"

Shutdown

1. Graceful shutdown (SIGTERM/SIGINT)
2. Close HTTP server (drain connections)
3. Exit process

HTTP Routes

Health Check

Endpoint: GET /health
Handler: server/src/routes/health.ts

Response:

{
  "status": "ok"
}

Use case: Kubernetes liveness probe, systemd health check


Version

Endpoint: GET /api/version
Handler: server/src/routes/version.ts

Response:

{
  "version": "1.0.0"
}

Source: package.json version field


Settings

Endpoint: GET /api/settings
Handler: server/src/routes/settings.ts

Response:

{
  "camillaControlWsUrl": "ws://localhost:1234",
  "camillaSpectrumWsUrl": "ws://localhost:1235"
}

Source: Environment variables CAMILLA_CONTROL_WS_URL and CAMILLA_SPECTRUM_WS_URL

Use case:

  • Client fetches connection defaults on first load (when localStorage is empty)
  • Enables production deployments to preconfigure CamillaDSP connection parameters
  • Returns null for URLs if environment variables are not set

Recovery Cache

Endpoint: GET /api/state/latest
Handler: server/src/routes/config.ts

Response: Full CamillaDSP config JSON

Use case:

  • Client reconnects after crash/reload
  • Retrieves last-applied DSP state
  • Avoids starting from empty config

Endpoint: PUT /api/state/latest
Handler: server/src/routes/config.ts

Request body: Full CamillaDSP config JSON

Response:

{
  "message": "Config saved successfully"
}

Use case:

  • Client uploads config to DSP
  • Writes-through to backend for recovery
  • Non-fatal if fails (best-effort)

File: server/data/latest_dsp_state.json


Preset Library

Endpoint: GET /api/configs
Handler: server/src/routes/configs.ts

Response: Array of ConfigMetadata objects

[
  {
    "id": "autoeq--headphones--sennheiser-hd-6xx",
    "configName": "Sennheiser HD 6XX",
    "file": "autoeq/headphones/Sennheiser HD 6XX.json",
    "mtimeMs": 1234567890123,
    "size": 2048,
    "presetType": "eq",
    "source": "autoeq",
    "readOnly": true,
    "category": "headphones",
    "manufacturer": "Sennheiser",
    "model": "HD 6XX"
  },
  {
    "id": "my-custom-eq",
    "configName": "My Custom EQ",
    "file": "My Custom EQ.json",
    "mtimeMs": 1234567890456,
    "size": 512,
    "presetType": "pipeline",
    "source": "user",
    "readOnly": false
  }
]

ID generation: Kebab-case from relative path + filename
Name extraction: From JSON name (EQ presets) or configName (pipeline configs)

Includes:

  • User-created presets (top-level + subdirs)
  • AutoEQ library presets (imported from AutoEQ database)

Endpoint: GET /api/configs/:id
Handler: server/src/routes/configs.ts

Response: Pipeline-config JSON (see pipelineConfigMapping.ts)

Use case: Load preset into UI

Note: AutoEQ presets (EqPresetV1 format) are converted on-the-fly to PipelineConfig format (legacy filterArray) for client compatibility.


Endpoint: PUT /api/configs/:id
Handler: server/src/routes/configs.ts

Request body: Pipeline-config JSON

Response:

{
  "success": true
}

Use case: Save current EQ/pipeline as preset

Protection: Returns 403 if ID matches a read-only preset (e.g., AutoEQ library)

File: server/data/configs/<id>.json (converted from kebab-case ID)


Services

ConfigStore (configStore.ts)

Purpose: Atomic single-file persistence

Key functions:

readConfig(filepath)

  • Reads JSON file
  • Returns parsed object
  • Throws if file missing or invalid JSON

writeConfig(filepath, data)

  • Writes JSON file atomically
  • Write to temp file → rename (atomic on POSIX)
  • Creates parent directories if needed
  • Size limit: 1MB

Atomic write flow:

1. Write to <filepath>.tmp
2. fsync() to flush kernel buffers
3. Rename <filepath>.tmp → <filepath>
4. Delete temp file on error

Error handling:

  • Throws AppError with error code
  • Caller decides retry/fallback

ConfigsLibrary (configsLibrary.ts)

Purpose: Preset library management (user presets + AutoEQ library)

Key functions:

listConfigs()

  • Returns metadata for all presets (user + AutoEQ)
  • Performance optimization: Uses autoeq/index.json manifest for fast AutoEQ lookups (O(1) read vs recursive scan)
  • Falls back to full recursive scan if manifest missing
  • Excludes index.json files from scan results
  • Sorts by name

getConfig(id)

  • Reads preset file by ID
  • Returns PipelineConfig format
  • AutoEQ conversion: EqPresetV1 files are converted on-the-fly to PipelineConfig (legacy filterArray format)

saveConfig(id, data)

  • Writes preset to server/data/configs/<id>.json
  • Read-only enforcement: Returns 403 if ID matches a preset with readOnly: true
  • Uses atomic write via configStore.writeConfig()

ID normalization:

  • Kebab-case from relative path + filename
  • Subdirectories encoded: /--
  • Example: "autoeq/headphones/Sennheiser HD 6XX.json""autoeq--headphones--sennheiser-hd-6xx"

AutoEQ Library:

  • Pre-imported headphone/IEM EQ presets from AutoEQ database
  • Located in server/data/configs/autoeq/<category>/
  • Marked as readOnly: true (cannot be overwritten)
  • Format: EqPresetV1 (converted to PipelineConfig on load)
  • Manifest file (autoeq/index.json) enables fast cold-start (no filesystem scan)

ShellExec (shellExec.ts)

Purpose: Safe shell command execution

Features:

  • Timeout protection (default: 10s)
  • Output size limits (default: 1MB)
  • Command whitelist (optional)
  • Abort on timeout/size exceeded

Status: Not used in production (included for future tooling)


Error Handling

AppError Class (types/errors.ts)

Purpose: Structured error responses

Shape:

class AppError extends Error {
  code: ErrorCode;
  statusCode: number;
  details?: any;
}

Error codes:

  • ERR_CONFIG_NOT_FOUND (404)
  • ERR_CONFIG_INVALID_JSON (400)
  • ERR_CONFIG_READ_FAILED (500)
  • ERR_CONFIG_WRITE_FAILED (500)
  • ERR_CONFIG_TOO_LARGE (413)

Fastify error handler:

  • Catches AppError instances
  • Returns JSON with { error, code, details? }
  • Logs stack trace to console

Configuration

Environment Variables

SERVER_PORT (default: 3000)

  • HTTP server port

SERVER_HOST (default: 0.0.0.0)

  • Network interface binding
  • Use 127.0.0.1 for localhost-only (behind reverse proxy)

CONFIG_DIR (default: ./data)

  • Base data directory (relative to WorkingDirectory)

CONFIGS_DIR (optional, default: <CONFIG_DIR>/configs)

  • Preset library directory

LOG_LEVEL (default: info)

  • Pino log level (error, warn, info, debug, trace)

NODE_ENV (default: development)

  • production → serves built frontend
  • development → API only (Vite serves frontend)
  • In development: server loads .env from server/.env (preferred) or repo root

CAMILLA_CONTROL_WS_URL (optional)

  • Default CamillaDSP control WebSocket URL
  • Example: ws://localhost:1234
  • Returned via GET /api/settings for client auto-configuration

CAMILLA_SPECTRUM_WS_URL (optional)

  • Default CamillaDSP spectrum WebSocket URL
  • Example: ws://localhost:1235
  • Returned via GET /api/settings for client auto-configuration

SERVER_READ_ONLY (default: false)

  • When true, blocks write operations (PUT/POST/PATCH/DELETE) to /api/*
  • CamillaDSP control from browser remains fully functional (WebSocket bypass)
  • Use for safer public exposure

Path Resolution (configPaths.ts)

Functions:

getConfigDir()

  • Returns absolute path to config directory
  • Checks CONFIG_DIR env, fallback to ./data

getConfigsDir()

  • Returns <dataDir>/configs

getLatestStatePath()

  • Returns <dataDir>/latest_dsp_state.json

Ensures:

  • Paths are absolute
  • Directories exist (creates if missing)

Static File Serving

Production Mode (NODE_ENV=production)

Behavior:

  • Serves server/dist/client/* at /
  • SPA fallback: All non-API routes → index.html
  • API routes prioritized (/api/*, /health)

Build artifact:

  • npm run build copies client/dist/server/dist/client/

Development Mode

Behavior:

  • Does NOT serve frontend (Vite dev server on 5173)
  • API only on port 3000
  • CORS not needed (Vite proxies /api/* to 3000)

Logging

Logger: Pino (server/src/logger.ts)

Log levels:

  • error - Unrecoverable errors
  • warn - Recoverable errors (e.g., file read failures)
  • info - Startup, shutdown, HTTP requests
  • debug - Detailed operation traces

Output:

  • Console (colorized with pino-pretty in dev)
  • JSON structured logs in production

Request logging:

  • Fastify plugin logs all requests
  • Format: GET /api/configs 200 12ms

Testing

Framework: Jest

Test types:

  • Unit tests for services (configStore, configsLibrary)
  • Integration tests for routes (routes.test.ts)

Run tests: npm test (from server directory)

Coverage: npm run test:coverage


Security

Input Validation

JSON schema validation:

  • Fastify validates request bodies against schemas
  • Rejects invalid payloads with 400

File path sanitization:

  • Config IDs must be alphanumeric + hyphens
  • Prevents directory traversal (../../etc/passwd)

Size limits:

  • Request body: 1MB max (Fastify default)
  • Config files: 1MB max (enforced by configStore)

No Authentication/Authorization

Current state: No auth layer

Rationale:

  • Designed for trusted LAN deployment
  • User responsible for network security

Future: Add auth middleware if needed (JWT, basic auth)


Performance

File I/O

  • Async operations only (no blocking)
  • Atomic writes prevent corruption
  • No caching (configs small, infrequent access)

HTTP

  • Keep-alive enabled (default)
  • Compression (gzip/brotli) via Fastify plugin

Startup

  • Lazy directory creation (on-demand)
  • No config preloading (load on first request)

Failure Modes

Data Directory Missing

  • Creates on startup (via configPaths.ts)
  • Logs warning if creation fails
  • Server starts anyway (degraded)

Config File Corrupted

  • Read error logged
  • Returns 500 to client
  • Does not crash server

Disk Full

  • Write fails with ERR_CONFIG_WRITE_FAILED
  • Temp file cleaned up
  • Returns 500 to client

Deployment

Production Build

npm run build

Output:

  • server/dist/ - Compiled TypeScript
  • server/dist/client/ - Built frontend

Start Production Server

npm run start

Runs: node server/dist/index.js

Environment:

  • Set NODE_ENV=production
  • Set SERVER_PORT if needed
  • Set DATA_DIR for custom path

Extension Points

Add New Endpoint

  1. Create route handler in server/src/routes/
  2. Register in server/src/app.ts
  3. Add tests in server/src/__tests__/

Add New Service

  1. Create service in server/src/services/
  2. Export functions, document contracts
  3. Add unit tests

Add Persistence Layer

  1. Extend configStore.ts with new file format
  2. Or add new service (e.g., database.ts for SQLite)

Next Steps