Deployment

Diminuendo is designed for minimal operational overhead. A single binary, a data directory, and a set of environment variables are all that is required. There is no external database to provision, no message broker to configure, and no migration tool to run.

Prerequisites

RequirementDetails
BunVersion 1.0 or later
PodiumA reachable Podium coordinator instance
Auth0An Auth0 application configured for JWT issuance and verification
Data directoryA writable directory for SQLite database files

Building

Diminuendo compiles to a single bundled file using Bun’s built-in bundler:
bun build src/main.ts --target=bun --outdir=dist --minify
This produces a single dist/main.js that includes all dependencies. The output can be run directly:
bun dist/main.js

Docker

A minimal Dockerfile for production:
FROM oven/bun:1

WORKDIR /app

# Copy package files and install
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production

# Copy source
COPY src/ src/
COPY tsconfig.json ./

# Create data directory
RUN mkdir -p /data

ENV DATA_DIR=/data
ENV NODE_ENV=production

EXPOSE 8080

CMD ["bun", "src/main.ts"]
For smaller images, use the bundled output instead of the full source tree:
FROM oven/bun:1-slim
WORKDIR /app
COPY dist/main.js .
RUN mkdir -p /data
ENV DATA_DIR=/data
ENV NODE_ENV=production
EXPOSE 8080
CMD ["bun", "main.js"]

Docker Compose

services:
  gateway:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - gateway-data:/data
    environment:
      PORT: 8080
      NODE_ENV: production
      DATA_DIR: /data
      AUTH_CLIENT_ID: ${AUTH_CLIENT_ID}
      AUTH_CLIENT_SECRET: ${AUTH_CLIENT_SECRET}
      AUTH_URL: ${AUTH_URL}
      PODIUM_URL: ${PODIUM_URL}
      PODIUM_API_KEY: ${PODIUM_API_KEY}
      ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
    restart: unless-stopped

volumes:
  gateway-data:

Data Persistence

The DATA_DIR directory must be persisted across container restarts and deployments. It contains all tenant and session SQLite databases:
$DATA_DIR/
  tenants/{tenantId}/registry.db
  sessions/{sessionId}/session.db
If the data directory is ephemeral (e.g., a container’s local filesystem without a volume mount), all session data will be lost on container restart. Always mount DATA_DIR as a persistent volume.

Backup

SQLite databases can be backed up by copying files. For a consistent backup during operation:
  1. Per-session: Copy the session directory. SQLite WAL mode ensures the copy is consistent even if the writer is active (the WAL file is included automatically).
  2. Per-tenant: Copy the tenant directory. This captures the registry and all session databases.
  3. Full backup: Copy the entire DATA_DIR.
For point-in-time recovery, use filesystem snapshots (ZFS snapshots, EBS snapshots, etc.) on the volume containing DATA_DIR.

TLS Termination

Diminuendo serves plain HTTP and WebSocket. TLS should be terminated at the load balancer or reverse proxy:
Client (wss://) → Load Balancer (TLS termination) → Gateway (ws://, port 8080)
The gateway includes HSTS and security headers in all HTTP responses. Configure your load balancer to preserve these headers or add them at the edge.

Reverse Proxy Configuration (nginx)

upstream gateway {
    server 127.0.0.1:8080;
}

server {
    listen 443 ssl;
    server_name gateway.example.com;

    ssl_certificate /etc/ssl/certs/gateway.crt;
    ssl_certificate_key /etc/ssl/private/gateway.key;

    location /ws {
        proxy_pass http://gateway;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    location /health {
        proxy_pass http://gateway;
    }
}
The proxy_read_timeout and proxy_send_timeout must be set high enough to accommodate long-lived WebSocket connections. The default nginx timeout of 60 seconds will cause premature disconnection.

Graceful Shutdown

When the gateway receives SIGTERM or SIGINT, it executes a graceful shutdown sequence:
1

Signal Received

The process signal handler unblocks the main Effect fiber.
2

Broadcast Shutdown

A server_shutdown event (with reason "deployment") is published to every active tenant and session topic. Connected clients receive notification that the server is going down.
3

Drain WebSocket Buffers

A 500ms delay allows shutdown events to drain through WebSocket send buffers.
4

Flush SQLite Writes

The WorkerManager’s shutdown() method flushes all buffered writes in the writer worker and closes all database handles. This ensures no writes are lost.
5

Stop Server

server.stop(true) forcefully closes all remaining WebSocket connections and stops the HTTP server.
6

Force Exit Timeout

A 10-second timeout guards against shutdown hangs. If the graceful sequence does not complete within 10 seconds, process.exit(1) is called.
const FORCE_EXIT_TIMEOUT_MS = 10_000

const forceExitTimer = setTimeout(() => {
  console.error(`Graceful shutdown timed out after ${FORCE_EXIT_TIMEOUT_MS}ms — forcing exit`)
  process.exit(1)
}, FORCE_EXIT_TIMEOUT_MS)
Configure your container orchestrator’s stop grace period to exceed the 10-second force exit timeout. For Kubernetes, set terminationGracePeriodSeconds: 15. For Docker, set stop_grace_period: 15s.

Horizontal Scaling

Diminuendo supports horizontal scaling through tenant-affinity routing. Since there is no shared state between instances, each instance independently manages its own set of tenants.

Load Balancer Configuration

The load balancer must route all requests for a given tenant to the same gateway instance. Two approaches:
Use cookie-based or IP-based sticky sessions. The first WebSocket connection from a client is routed to any available instance. Subsequent connections from the same client are routed to the same instance.This works for single-tenant deployments or when each client connects to only one tenant.

Stale Session Recovery

When an instance restarts (whether due to deployment, scaling, or crash), it performs stale session recovery:
  1. Enumerates all known tenant IDs from the data/tenants/ directory
  2. For each tenant, queries the registry for sessions with non-inactive status
  3. Resets each stale session to inactive
This is safe because Podium connections do not survive process death. The agent’s compute instance has already been reclaimed. When clients reconnect and join these sessions, they receive an inactive state snapshot and can re-activate the session. Recovery runs as a background fiber and does not block incoming connections. Up to 4 tenants are reconciled concurrently.

Scaling Considerations

ConcernCurrent ModelScaling Limitation
Session stateIn-memory per instanceSession must reconnect to same instance
Event fan-outBun pub/sub (per-process)Cross-instance events require Redis/NATS
Rate limitingPer-instance countersDistributed attackers bypass per-instance limits
BillingPer-instance credit reservationShared tenant across instances needs shared ledger
For a detailed analysis of horizontal scaling properties and limitations, see Scalability & Horizontal Scaling.

Environment Checklist

Before deploying to production, verify:
  • NODE_ENV=production (or DEV_MODE is unset/false)
  • AUTH_CLIENT_ID, AUTH_CLIENT_SECRET, AUTH_URL are set
  • PODIUM_URL and PODIUM_API_KEY are set
  • ALLOWED_ORIGINS includes the frontend’s origin
  • DATA_DIR points to a persistent volume
  • TLS is terminated at the load balancer
  • Health check configured: GET /health expecting 200
  • Container stop grace period exceeds 10 seconds
  • Log aggregation configured for JSON log output