Rust gateway
Axum + tokio. BYOK routing across 205 routable providers (6 native adapters
- 199 OpenAI-compatible catalog entries). One typed admission pipeline shared
by the chat, embeddings and messages routes. Predictive layer inline,
observe-first by default. Emits its own spans.
crates/gateway/
Rust ingest workers
OTLP receiver, NATS JetStream consumer, batched ClickHouse inserts
acknowledged after the write. The only component that writes spans.
crates/ingest/Next.js 15 dashboard
App Router, Tailwind 4, in-house components. Trace viewer with waterfall,
transcript-spine and agent-lane views, rendered in the DOM. No ClickHouse
client.
apps/web/TypeScript MCP server
Read-only, tenant-scoped, bearer-token auth.
npx @tracelanedev/mcp,
stdio or streamable-HTTP transport. apps/mcp/Python eval orchestrator
Wraps DeepEval + Ragas + Inspect AI behind a unified API. Runs in CI.
evals/Data flow
The other entry points all go through the same gateway; only two read storage directly: Solid edges are the hosted product’s path. Dashed edges exist only in a particular configuration: the ingest worker’s own OTLP receiver is reachable inside the deployment network over mTLS, or on a self-host install as a plaintext port bound to the same host; the MCP server reads ClickHouse directly only on its stdio transport withCLICKHOUSE_URL set, and its HTTP transport
refuses to start with that variable present. The ClickHouse ledger copy and the
best-effort Sigstore Rekor anchor appear in the ledger diagram below.
Write path
A request enters the gateway through one typed admission pipeline shared by/v1/chat/completions, /v1/embeddings and /v1/messages. Its order is fixed
in code and checked at compile time: authenticate (a WorkOS JWT or a tlane_
API key, resolved to an internal tenant UUID) → scope → parse → entitlements →
rate limit → key and workspace budgets → predictive layer (observe-first: its
verdict is recorded on the request and enforced only when the operator sets
TRACELANE_PREDICTIVE_ENFORCE=1) → audit publish. The audit publish is an
acknowledged JetStream write, and it fails closed: if the ledger cannot take the
event the request is refused with 503 audit_unavailable, before any byte
reaches a provider.
After admission the chat handler resolves the provider and the tenant’s own
provider key, runs the inline guardrail rails over the request (a rail verdict
is recorded to the ledger either way; a security rail can refuse the call with
403), wraps tool-result content in an <UNTRUSTED_USER_DATA> sentinel, and
dispatches with one same-provider retry. Cross-provider failover is off by
default and enabled per request with X-Tracelane-Failover: cross-provider;
a failover that served the request is named on the span.
The gateway’s own spans, and any OTLP batch a client sends to POST /v1/traces,
are published to NATS JetStream with an acknowledged publish and a per-span
Nats-Msg-Id, so a republished span is dropped at the boundary. The ingest
consumer takes the tenant from the subject the message arrived on, never from
the span body, tail-samples (errors and spans carrying an intervention are
always kept; the default keep rate is 100%), batches the inserts, and acknowledges a
message only after the ClickHouse write returns — a failed insert leaves the
message unacknowledged and JetStream redelivers it.
Span deletion follows the plan’s queryable window (Free 30 days, every paid tier
730 days), applied by an entitlement-driven sweep job that reads the plan table;
the table’s own TTL is a backstop behind it. Retention is not customer-configurable.
Read path
The dashboard has no ClickHouse client. It forwards the signed-in user’s WorkOS token to the gateway’s read routes (/v1/traces, /v1/sessions, /v1/slo,
/v1/costs, /v1/guardrails/*, /v1/query/signatures and the rest), which
resolve the tenant from the validated claim — never from the path, query string
or body — bind it first in a parameterised WHERE tenant_id = ?, and wrap the
SELECT in a per-tier SETTINGS clause that caps rows read, memory and execution
time. A trace that belongs to another tenant answers the same 404 as one that
does not exist. Two CI guards hold this: one fails any query against
tracelane.* whose literal carries no tenant filter, the other fails any
ClickHouse read outside the capped wrapper.
The ledger
Each admitted request is appended as a row in a per-tenant hash chain, and guardrail verdicts are appended the same way:row_hash = SHA-256(tenant_id ‖ seq ‖ type ‖ actor ‖ canonical_payload ‖ prev_hash), domain-separated and length-prefixed over the RFC 8785 canonical
payload. A single head-writer task inside the gateway assigns seq and commits
the row and the chain head in one Postgres transaction; the ClickHouse
audit_log table is a derived copy written after that commit, and the boot-time
reconcile that rebuilds it recomputes each candidate row’s content hash before
adopting it into the canonical store.
Batches close every 100 rows or after 24 hours, whichever comes first. Each
batch’s RFC 6962 Merkle root is signed with the tenant’s own Ed25519 key, and
the signed attestation binds the root to the batch’s anchor state so a stripped
or downgraded export fails offline verification. The root is additionally
submitted to the public Sigstore Rekor v2 transparency log on a best-effort
basis when the deployment configures a Rekor endpoint; an anchored batch carries
the resolved inclusion proof, checkpoint and log index, and an unanchored batch
stays signed-locally and still verifies its chain. Anchoring is per batch, never
per event. The export
(GET /v1/audit/export) streams the canonical Postgres rows and anchor records
as NDJSON and fails closed: a read error aborts the transfer rather than
returning a partial ledger. tlane verify recomputes the chain offline; the
Rust, TypeScript and Python reference verifiers are held to the same verdicts on
committed conformance vectors in CI.
A self-host install with no Postgres control plane keeps a process-local chain
and writes the ClickHouse copy on a best-effort basis; that tier has no
canonical store and should not be read as the same durability as the hosted
ledger.
Source-of-truth split
Performance
The gateway-overhead figure is measured: 2 ms p50 / 4 ms p95 / 5 ms p99 over the direct-to-mock baseline, 2026-09-07, LiteLLM’s AIGatewayBench harness on a 4 vCPU host, two runs, capture on. Method, caveats and the other gateways are on the benchmark page. The benchmark script enforces a p99 below 25 ms as a k6 threshold; that job runs on demand, not on every change. The predictive layer’s<30ms p50 / <50ms p95 / <100ms p99 figure is an
engineering target, not a measurement on production-equivalent hardware — see
Predictive guardrails. No other latency or throughput
figure for ingest, the dashboard, the MCP server or the eval layer has been
measured or is enforced, so none is published here.
Security posture
- BYOK. Provider keys are sealed with AES-256-GCM (
ringAEAD) and bound to the tenant and provider. The gateway uses the deployment master key by default; an entitled workspace can configure a customer KMS wrapped tenant data key for provider-key envelopes through/v1/security/kms. - mTLS for ingest, TLS 1.3 only. The ingest worker’s OTLP receiver terminates mTLS itself and is built with TLS 1.3 as its only protocol version; plaintext mode exists for local development.
- Tenant identity comes from a validated credential. Read routes resolve the tenant from the claim and bind it in SQL; the query wrapper adds the resource caps and leaves the tenant filter to the caller, which the CI guard checks per query literal.
- Outbound calls to customer-supplied URLs are pinned. The SSRF guard validates the scheme and resolved addresses and returns the addresses it checked; the alert-webhook and Vertex token-endpoint clients connect to exactly those addresses, so a DNS answer that changes after validation is not followed.
- Per-service data-plane credentials. The gateway and ingest processes authenticate to NATS with their own credentials and derive a private reply-inbox prefix from that credential, so one service cannot receive the other’s deliveries. CI proofs run the real clients against the deployment’s NATS authorization and ClickHouse grants and assert the refusals, not only the grants.
- Prompt-injection-aware. Tool-result messages and tool-result content
parts in a proxied request are wrapped in
<UNTRUSTED_USER_DATA>before the model sees them; user turns are left alone. - Trusted Publishing OIDC only. npm, PyPI and crates.io publishes mint short-lived tokens from the workflow’s OIDC identity; the release workflows reference no stored registry secret.
- Sigstore signing + CycloneDX SBOM. Live on tagged release artifacts since
v0.2.3— each binary ships a.cosign.bundle,sbom.cyclonedx.jsonis attached and signed — as well as on the container image and a weekly repository SBOM.
Related
- Predictive guardrails — what runs inline
- Audit ledger — the chain, signing and anchoring in detail
- Providers — 205 routable LLM providers
- API reference — REST surface
- Decisions — architectural decisions