Architecture Overview
Diminuendo is a WebSocket gateway built on three foundational technology choices: Bun for the runtime, Effect TS for business logic, and SQLite for persistence. Each choice was made deliberately to minimize operational complexity while maximizing correctness and performance.Runtime: Bun
Bun provides three capabilities that are essential to Diminuendo’s architecture:- Native WebSocket server with built-in pub/sub — Bun’s
Bun.serve()supports topic-based publish/subscribe directly in the WebSocket handler, eliminating the need for an external message broker (Redis pub/sub, NATS, etc.) for single-instance deployments - Native SQLite via
bun:sqlite— synchronous, in-process SQLite with prepared statements, WAL mode, and zero serialization overhead - Fast startup — the gateway starts in under 200ms, enabling rapid restart cycles during development and minimal downtime during deployment
Bun’s WebSocket pub/sub is per-process. For multi-instance deployments, a broadcast layer (Redis pub/sub or similar) would be needed. The current architecture is designed for vertical scaling — a single instance handling thousands of concurrent sessions.
Business Logic: Effect TS
All business logic is written using Effect, a TypeScript library for building type-safe, composable programs. Effect provides:- Typed errors — every function declares its failure modes in the type signature; there are no thrown exceptions
- Dependency injection — services are composed via
Layer, not imported as singletons - Structured concurrency — background fibers are tracked and interrupted on shutdown
- Resource management — database connections, WebSocket handles, and timers are acquired and released within scoped effects
Storage: SQLite with WAL Mode
Diminuendo uses SQLite exclusively for persistence — no Postgres, no Redis, no external database. The storage architecture uses a hierarchy of databases:Per-Tenant Isolation
A query against one tenant’s data cannot accidentally touch another tenant’s rows. There is no
WHERE tenant_id = ? clause to forget — the databases are physically separate files.Zero-Contention Writes
Concurrent writes to different sessions hit different SQLite files. WAL mode allows concurrent readers and a single writer per database, which is sufficient for the gateway’s access pattern (one writer per session).
Trivial Deletion
Deleting a session means deleting a directory. No
DELETE FROM across multiple tables, no orphaned rows, no vacuum needed.Horizontal Scaling
Moving a tenant to a different gateway instance means moving a directory. No data migration, no schema changes, no downtime.
Transport Layer
The transport layer consists of two components: the HTTP/WebSocket server and the Broadcaster.Server (src/transport/server.ts)
Bun.serve() handles both HTTP (health checks) and WebSocket (client connections) on a single port. The WebSocket lifecycle is:
WsData): client ID, authentication identity, topic subscriptions, and the last event sequence number.
Broadcaster (src/transport/Broadcaster.ts)
The Broadcaster abstracts Bun’s native pub/sub into an Effect service. It provides two publishing channels:
- Session events — published to
session:{sessionId}, received by all clients that have joined that session - Tenant events — published to
tenant:{tenantId}:sessions, received by all authenticated clients in the tenant (used for session list updates)
server_shutdown event to every active topic before closing connections.
Module Layout
The gateway source is organized into eleven modules, each with a single responsibility:| Module | Directory | Responsibility |
|---|---|---|
| Auth | src/auth/ | JWT verification (Auth0), identity extraction, RBAC permission checks, tenant membership management |
| Transport | src/transport/ | WebSocket server, HTTP health endpoint, Broadcaster service |
| Protocol | src/protocol/ | Effect Schema definitions for all 21 client message types; runtime validation and parsing |
| Session | src/session/ | Session lifecycle management, MessageRouter (the central dispatch), ConnectionState, SessionState machine, event handlers |
| Automation | src/automation/ | Automation store, scheduler, run execution, heartbeat configuration, and inbox management |
| Domain | src/domain/ | Business rules — PodiumEventMapper (translates agent events to client events), BillingService (credit reservation and settlement) |
| Upstream | src/upstream/ | External service clients — PodiumClient (agent orchestration), EnsembleClient (LLM inference) |
| Security | src/security/ | CSRF protection, security headers, error message sanitization, auth rate limiting |
| Resilience | src/resilience/ | RetryPolicy (exponential backoff with jitter), CircuitBreaker (failure threshold with cooldown) |
| Observability | src/observability/ | Health endpoint (deep health checks against Podium and Ensemble), OpenTelemetry tracing |
| DB | src/db/ | Schema migrations, WorkerManager (batched async writes to SQLite) |
Layer Composition
Diminuendo uses Effect’sLayer system for dependency injection. The composition is defined in src/main.ts:
Context.Tag with a corresponding Live implementation. No service imports another service directly — dependencies flow through the Layer graph, making them explicit, testable, and replaceable.
Data Flow
The complete path of a client message through the gateway:1
Wire
Client sends JSON over WebSocket.
Bun.serve receives raw bytes and invokes the message() handler.2
Validation
The raw string is parsed as JSON, then validated against
ClientMessage using Schema.decodeUnknownEither. Invalid messages are rejected with an error event — they never reach business logic.3
Authentication Check
The server verifies that the connection has completed authentication (the
authenticated flag on WsData). Unauthenticated connections can only send authenticate messages.4
Rate Limiting
A per-connection sliding window rate limiter checks whether the client has exceeded 60 messages per 10-second window. Rate-limited messages are rejected with
RATE_LIMITED.5
Routing
MessageRouter.route(identity, message) dispatches to the appropriate handler based on message.type. The router returns a RouteResult: either respond (send to this client), broadcast (publish to session topic), or none (no response needed).6
Execution
The handler performs its work — querying the registry, creating a Podium connection, reserving billing credits, writing to SQLite, or forwarding to an upstream service.
7
Response
For
respond results, the event is sent directly to the requesting client. For session-mutating operations (create, archive, delete, rename), the event is also broadcast to the tenant topic so other clients can update their session lists.8
Streaming
For
run_turn, the response is broadcast — all events from the agent are streamed to the session topic via the Broadcaster. The startEventStreamFiber consumes the Podium event stream, maps each event through PodiumEventMapper, publishes to the session topic, persists important events to SQLite, and dispatches to specialized event handlers for state management.Per-Tenant Isolation
Diminuendo enforces tenant isolation at three levels:-
Authentication — every JWT contains a
tenant_idclaim. The gateway extracts this during authentication and attaches it to the connection’s identity. All subsequent operations are scoped to this tenant. -
Storage — each tenant has its own SQLite registry database at
data/tenants/{tenantId}/registry.db. Session databases are stored atdata/sessions/{sessionId}/session.db. A session can only be accessed by providing asessionIdthat exists in the requesting tenant’s registry. -
Pub/Sub — topic subscriptions are namespaced by tenant. A client authenticated as tenant
acmesubscribes totenant:acme:sessions; it will never receive events intended for tenantglobex.
What’s Next
- Effect TS Patterns — deep dive into the specific Effect patterns used throughout the codebase
- Session State Machine — the 7-state lifecycle and its transition rules
- Multi-Worker SQLite — how the WorkerManager batches and isolates database writes
- Automation System — scheduled tasks, proactive agent awareness, and automation management
- Security — CSRF, rate limiting, error sanitization, and RBAC