Regressa

Regressa architecture

Regressa is a multi-tenant LLM observability and prompt-regression platform. This document describes the system as built in this repository: components, data flow, storage, tenancy, and the trade-offs behind them.

System overview

flowchart LR
  subgraph Apps["Customer application"]
    SDK["SDK
@regressa/node · regressa-sdk · Go · Java"] OTEL["OpenTelemetry exporter"] end SDK -- "POST /v1/traces" --> ING OTEL -- "POST /v1/otlp/traces" --> ING subgraph Regressa ING["ingest-api
Fastify"] Q1[("Redis · BullMQ
regressa-traces")] W1["worker · persist
version detection · redaction"] Q2[("regressa-evals")] W2["worker · evals
LLM judge · golden set · custom JS"] W3["worker · alerts
rules engine every 60s"] Q3[("regressa-notify")] W4["worker · notify
Slack · webhook · email"] W5["worker · maintenance
retention · weekly digest"] DB[("PostgreSQL + TimescaleDB
schema regressa")] WEB["web · Next.js 14
dashboard · public REST API · gate"] end ING --> Q1 --> W1 --> DB W1 --> Q2 --> W2 --> DB W3 --> DB W3 --> Q3 --> W4 W5 --> DB WEB --> DB W4 --> Slack["Slack / webhooks / SMTP"] CI["CI job
npx @regressa/node gate"] -- "GET /api/v1/gate" --> WEB User["Team"] --> WEB

Components

Component Path Role
Ingest API apps/ingest-api Authenticates project keys, validates payloads item by item, assigns ids, applies budget sampling, enqueues. Stateless; scale horizontally.
Worker apps/worker Five BullMQ consumers in one process (persist, evals, alerts, notify, maintenance). Scale by running more replicas; job schedulers are deduplicated by id.
Web apps/web Dashboard (server components read Postgres directly), public REST API, CI gate endpoint, OIDC SSO, Stripe.
DB package packages/db SQL migrations (source of truth), Drizzle models, key hashing, seed.
Shared types packages/shared-types Zod schemas for the wire format, model pricing, redaction presets, anomaly math, plan limits.
SDKs packages/sdk-* Buffered clients that wrap provider SDKs and ship traces with retries. Dependency-free where possible.

Request lifecycle

sequenceDiagram
  participant App
  participant SDK
  participant Ingest as ingest-api
  participant Redis
  participant Persist as worker/persist
  participant PG as TimescaleDB
  participant Evals as worker/evals
  App->>SDK: openai.chat.completions.create(...)
  SDK->>App: response (unchanged)
  SDK-->>SDK: buffer trace (id, latency, tokens, template)
  SDK->>Ingest: POST /v1/traces (batch, X-Regressa-Project-Key)
  Ingest->>Ingest: sha256(key) lookup (60s cache) · zod per item · budget sampling
  Ingest->>Redis: enqueue persist job
  Ingest-->>SDK: 202 {accepted, rejected, errors}
  Redis->>Persist: job
  Persist->>PG: upsert prompt_templates / prompt_versions (hash of normalized template)
  Persist->>Persist: redact PII per project rules
  Persist->>PG: INSERT traces ON CONFLICT DO NOTHING
  Persist->>Redis: eval jobs (sample_rate, template scope)
  Redis->>Evals: job
  Evals->>PG: INSERT eval_results

Idempotency: the ingest API assigns a UUID and timestamp to every trace before enqueueing. The primary key on traces is (id, created_at), so a retried persist job cannot duplicate rows.

Prompt-version detection (the regression core)

Every trace may carry prompt_template: { name, raw }. The worker normalizes raw (CRLF to LF, trailing whitespace trimmed, outer blank lines removed), hashes it with SHA-256, and upserts prompt_versions keyed by (template, hash). A new hash becomes version N+1 and fires prompt_version_change rules immediately. All metrics (eval score, p95, error rate, cost) can then be compared per version, which is what the Prompts page, the CI gate, and the anomaly detector use.

Evals

Type Runs where Cost
llm_judge Anthropic Messages API with a forced submit_score tool call; 0-100 mapped to 0-1 judge model tokens, recorded per result
semantic_similarity OpenAI embeddings; best cosine similarity to cached golden embeddings embedding tokens
custom_function node:vm sandbox, 250ms timeout, no require/process free

Evals are sampled per definition (sample_rate) and optionally scoped to one template. The eval worker is rate-limited to 60 jobs/minute by default to protect provider quotas.

Alerting

Rules are evaluated every 60 seconds over rolling windows with per-rule cooldowns and minimum sample sizes.

Condition Semantics
gt / lt current window vs absolute threshold
pct_change current window vs the immediately preceding window
anomaly z-score of the current window vs the previous 24 windows (5% stddev floor)
budget metric month-to-date spend vs the project cap; also toggles ingest sampling
prompt_version_change fired inline by the persist worker

Notifications go through a separate queue with retries; delivery state is recorded on alert_events.

Storage

See docs/data-model.md for the table-by-table reference.

Tenancy and security

Scaling notes

Repository layout

apps/            web, ingest-api, worker
packages/        db, shared-types, sdk-node (+ gate CLI), sdk-python, sdk-go, sdk-java
infra/           docker-compose (TimescaleDB, Redis), terraform stub
docs/            product and operator documentation (published to GitHub Pages)
.github/         CI, Pages deploy, reusable regressa-gate action