Architecture
This page provides a technical overview of SideSeat’s architecture for developers who want to understand how the local workbench is built or contribute to the project.
System Overview
Section titled “System Overview”SideSeat runs as a single local binary that includes:
- HTTP Server (Axum) - REST API, OTLP ingestion, static file serving
- gRPC Server (Tonic) - Optional high-throughput OTLP ingestion
- Workbench UI (React) - Embedded static files
- Storage ports - DuckDB + SQLite by default, or ClickHouse + PostgreSQL for distributed deployments
Ingestion Pipeline
Section titled “Ingestion Pipeline”The ingestion pipeline processes incoming OTLP data through these stages:
1. Receive
Section titled “1. Receive”OTLP data arrives via:
- HTTP:
POST /otel/{project_id}/v1/traces(protobuf or JSON) - gRPC:
localhost:4317(protobuf)
2. Decode
Section titled “2. Decode”Protobuf messages are decoded into internal SpanData structures. The decoding preserves all original attributes.
3. Extract
Section titled “3. Extract”Framework detection and attribute extraction happen at ingestion time. Producer-specific knowledge is
declared in JSON under server/assets/rules/; sideseat-rule-assets embeds it and sideseat-domain
compiles it into typed plans consumed by ingestion. Adding a producer normally changes rules and fixtures,
not framework-name branches in Rust.
4. Enrich
Section titled “4. Enrich”Enrichment adds derived fields:
- Cost calculation: Look up model pricing, compute costs
- Preview generation: Truncate input/output for list views
- Timestamps: Normalize to microseconds
5. Persist
Section titled “5. Persist”Spans are batched and written to DuckDB. The schema preserves raw attributes in a JSON column for flexibility.
Database Architecture
Section titled “Database Architecture”SideSeat uses two databases:
Analytics Database (DuckDB/ClickHouse)
Section titled “Analytics Database (DuckDB/ClickHouse)”Stores telemetry data optimized for analytical queries:
- spans: Normalized span data with extracted fields
- Raw attributes: Preserved in JSON for ad-hoc queries
Default: DuckDB (embedded, zero config)
Optional: ClickHouse (external, high volume)
Transactional Database (SQLite/PostgreSQL)
Section titled “Transactional Database (SQLite/PostgreSQL)”Stores application state:
- projects: Project configuration
- api_tokens: Authentication tokens
- settings: User preferences
Default: SQLite (embedded, zero config)
Optional: PostgreSQL (external, HA)
Query Architecture
Section titled “Query Architecture”Message Normalization (SideML)
Section titled “Message Normalization (SideML)”SideSeat normalizes messages from different frameworks into a universal format called SideML:
This happens at query time via sideml/normalize.rs:
- Extract events from span
- Categorize by role (system/user/assistant/tool)
- Parse content into blocks (text, tool_use, tool_result, etc.)
- Deduplicate using content hashing
- Sort by birth time
Query Endpoints
Section titled “Query Endpoints”All query endpoints are prefixed with /api/v1/project/{project_id}/otel:
| Endpoint | Purpose |
|---|---|
GET /traces | List runs with filtering |
GET /traces/{id} | Get a single run |
GET /traces/{id}/messages | Get normalized messages |
GET /spans | List steps with filtering |
GET /sessions | List sessions |
GET /sse | Real-time event stream |
Real-time Streaming
Section titled “Real-time Streaming”SideSeat uses Server-Sent Events (SSE) for real-time updates:
sideseat-messaging is a typed facade over the queue port. The composition root selects an in-process,
Redis, or Redpanda adapter; stream topics carry durable ingestion work when configured, while broadcast
topics feed live SSE subscribers.
Configuration System
Section titled “Configuration System”Configuration is loaded from multiple sources with this priority (highest first):
- CLI arguments:
--port 8080 - Environment variables:
SIDESEAT_PORT=8080 - Working directory config:
./sideseat.json - User config:
~/.sideseat/sideseat.json - Defaults: Built-in values
// Priority chain examplelet port = cli.port .or(env_port) .or(workdir_config.port) .or(user_config.port) .unwrap_or(DEFAULT_PORT);Directory Structure
Section titled “Directory Structure”The layers are separate crates, so a forbidden dependency does not compile: server/crates/core names no driver
and nothing of SideSeat’s own, which is what makes the direction a fact rather than a convention.
server/crates/core/src/├── config.rs # Configuration loading├── cli.rs # CLI argument parsing├── constants.rs # Shared limits and defaults├── storage.rs # Storage layout└── utils/ # Pure utilitiesserver/crates/ports/src/├── traits.rs # Repository interfaces - no implementation may be named here├── error.rs # DataError, carrying each driver's message rather than its error type├── filters/ # The filter vocabulary, shared by both analytics adapters└── types/ # Shared DTOsserver/crates/├── core/ # Configuration, constants, storage layout, pure utilities├── ports/ # Traits, DTOs, filters, and shared errors├── domain/ # Reconstruction, governance, files, and search├── ingestion/ # OTLP lifecycle, staging, normalization, and persistence├── messaging/ # Typed streams and broadcasts over the queue port├── api/ # HTTP/gRPC transport and route handlers├── query-sql/ # Typed cross-engine query construction├── rule-assets/ # Deterministic embedding of framework rule JSON├── adapter-duckdb/ # Embedded analytics├── adapter-clickhouse/ # Server analytics├── adapter-sqlite/ # Embedded transactional store├── adapter-postgres/ # Server transactional store├── adapter-blob-storage/ # Filesystem and S3 blobs├── adapter-cache/ # In-memory and Redis cache├── adapter-topics/ # In-process, Redis, and Redpanda messaging├── adapter-secrets/ # Credential secret providers├── adapter-pricing/ # External pricing catalogue└── adapter-registrations-memory/ # Process-local agent registrationserver/src/├── main.rs # Process entry point├── lib.rs # Testable server library boundary├── app.rs # Composition root├── app/ # Adapter selection, lifecycle, and system workflows│ ├── background_tasks.rs│ ├── files.rs│ ├── providers.rs│ ├── providers/│ │ └── azure.rs│ ├── storage/│ │ ├── mod.rs│ │ ├── analytics.rs│ │ └── transactional.rs│ ├── system_commands.rs│ └── update.rs└── runtime/ # Process lifecycle and allocator instrumentation ├── allocation.rs ├── clock.rs ├── mod.rs └── shutdown.rsKey Design Decisions
Section titled “Key Design Decisions”Why DuckDB?
Section titled “Why DuckDB?”- Embedded: No external dependencies
- Analytical: Columnar storage for fast aggregations
- Compatible: SQL interface, easy migration to ClickHouse
Why SQLite?
Section titled “Why SQLite?”- Embedded: Single file, zero config
- ACID: Full transaction support
- Compatible: Easy migration to PostgreSQL
Why Rust?
Section titled “Why Rust?”- Performance: Low latency, high throughput
- Memory safety: No GC pauses, predictable performance
- Single binary: Easy distribution via npm
Why React (not Next.js)?
Section titled “Why React (not Next.js)?”- Embedded: Static files bundled in binary
- Simple: No SSR complexity for a local workbench UI
- Portable: Works with any HTTP server
Next Steps
Section titled “Next Steps”- API Reference — REST API documentation
- OpenTelemetry Reference — attribute extraction details
- Configuration Schema — all config options