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 - DuckDB (analytics) + SQLite (transactional)
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 happens at ingestion time:
// Detect framework from span attributeslet framework = detect_framework(&span.attributes);
// Extract GenAI-specific fieldslet model = get_first(attrs, &[ "gen_ai.request.model", "gen_ai.response.model", "llm.model_name",]);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/pipeline.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:
The Topic is an in-memory pub/sub system that broadcasts events to all connected clients.
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”server/src/├── app.rs # Main orchestrator├── core/│ ├── config.rs # Configuration loading│ ├── cli.rs # CLI argument parsing│ ├── constants.rs # All constants│ └── topic.rs # Pub/sub messaging├── data/│ ├── duckdb/ # DuckDB implementation│ ├── clickhouse/ # ClickHouse implementation│ ├── sqlite/ # SQLite implementation│ ├── postgres/ # PostgreSQL implementation│ └── traits.rs # Repository interfaces├── domain/│ ├── pricing/ # Cost calculation│ ├── sideml/ # Message normalization│ └── traces/ # Trace extraction/enrichment└── api/ └── routes/ # HTTP handlersKey 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