Core architecture
Four pieces moving one signal end to end: a request happens somewhere in your service, gets traced, and shows up on a dashboard. This is the shape of it.
The four pieces
Your service
npm package
The @opticsops/agent package loads into your Node.js process at startup. It patches the built-in http/https modules and undici (native fetch) so every HTTP call is observed automatically — no code changes, no framework plugin.
Ingest API
Fastify on Cloud Run
Receives traces and health-check summaries from every instrumented service, authenticates each request, and is the only component with credentials to the datastore. Also serves the dashboard.
Datastore
ClickHouse
Stores traces and call-graph data, scoped per project. Every query the ingest API runs is filtered to the calling project — one project's data is never visible from another's key.
Portal
Next.js on Vercel
Where you sign up, sign in, and manage your API key. It never talks to the datastore directly, and never sees your ClickHouse credentials — it goes through the same authenticated ingest API everything else does.
The agent and the portal never talk to each other, and neither ever holds a datastore credential — the ingest API is the only thing that does.
One request, start to finish
A request enters your service
The agent starts a span. If the request arrived from another instrumented service, it's linked to that service's trace via a standard W3C traceparent header — not a proprietary format.
Your handler calls another service
The agent starts a child span and injects the trace header into the outgoing call, so the next hop can link back to this one — automatically, however deep the call chain goes.
The response comes back
Fast and healthy → counted in a rolling aggregate, nothing exported. Slow or an error → the full linked trace for that request is captured and sent. This is tail-based sampling: the decision happens after the request finishes, so a failing request is never the one that got sampled out.
Data lands, scoped to your project
The ingest API authenticates the incoming request and writes it against your project only. Every read later — the dashboard, the API — is scoped the same way.
You watch it happen
The dashboard polls and streams live updates: which services exist, how they call each other, and the trace of anything that went wrong, in the order it actually happened.
Isolation model
Every project gets its own API key at signup. Every write and every read is scoped to the project that key belongs to — there's no shared view, no cross-project query path, and no way to see another project's services, traces, or call graph from your key. There's one datastore behind the whole platform, not one per customer; isolation is enforced in the application layer on every single query, not by physically separate infrastructure per project.
Curious what changed since this version? Read v2 →