Architecture
How the Fastify server, SQLite store, React dashboard, and Graphiti sidecar fit together. One process, three transports, MIT.
OpalServe is one Node.js process that runs three things at once: a Fastify HTTP server, an MCP gateway, and (optionally) a Graphiti Python sidecar. Storage is SQLite via sql.js, so the whole runtime is a single binary plus a single data file.
This page is a single-screen pass over the entire system. Each section is short.
Topology
External MCP servers (GitHub, Slack, Postgres, Filesystem, Graphiti)
│
stdio / sse
│
┌───────────────┴───────────────┐
│ OpalServe team server │
│ │
│ Registry Knowledge │
│ Auth + RBAC Audit │
│ Drift API Dashboard │
│ │
│ SQLite ── Fastify ── React │
└───────────────┬───────────────┘
│
HTTP + MCP
│
┌─────────────────────────┼─────────────────────────┐
Dev A Dev B CI/CD
Claude Desktop Cursor Slack bot
The HTTP server
Fastify v5. Plugins: @fastify/helmet for security headers, @fastify/cors for origin allow-listing, @fastify/rate-limit for per-IP and per-key throttling, @fastify/static to serve the built React dashboard. Every route schema is enforced by zod before handlers run.
The HTTP API lives at /api/v1/*. The dashboard is served as static assets from /dashboard. The MCP gateway (when running as opalserve start --mcp) speaks stdio rather than HTTP.
The registry
src/core/registry.ts. The single source of truth for every connected MCP server and every tool that server exposes. The callTool(toolId, args, CallContext) entry point proxies a tool invocation to the backend MCP server, records a usage_event row, and returns the result.
Every registered server has a stable name, a transport definition (stdio command or remote URL), and a discovered tools cache. The cache is rebuilt on reconnect or on the server's first connection.
Storage
Two tables matter:
usage_events, one row per tool call. Capturestool_id,user_id,started_at,ended_at,ok,error. Backs/api/v1/statsand/me/activity.audit_log, one row per auth event, server change, or admin action. Backsopalserve admin audit.
The schema lives at src/storage/migrations/. Migrations run automatically on startup. There is no migration tool, versions are expressed as numbered SQL files and applied in order.
Auth
API keys are HMAC-SHA256 hashed before storage. The HMAC secret is OPALSERVE_API_KEY_SECRET, auto-generated on first start and persisted to the config file. Passwords are scrypt-hashed using crypto.scrypt.
Every authenticated route attaches a request.user populated by the auth middleware in src/auth/middleware.ts. RBAC is enforced via a requireRole(role) decorator. Three roles exist, admin, developer, viewer.
The dashboard
A React 19 single-page app. Bundled at build time and shipped inside the npm tarball. Routing via React Router. State is server-driven, the dashboard polls /api/v1/* rather than maintaining its own store.
Three sections in the sidebar, Registry (Servers, Tools), Connections (Users, Context), Observe (Overview, Drift, Graph), plus Settings. The reorganization landed in v3.4 alongside Drift Detection.
The MCP gateway
OpalServe is itself an MCP server. When started with --mcp (or by an editor via opalserve link), it speaks the MCP stdio protocol and exposes every tool from every registered backend server through a single endpoint.
Editors point at OpalServe instead of configuring individual servers. When OpalServe re-syncs the registry, every editor inherits the new state without a per-machine config edit.
The Graphiti sidecar
When opalserve enable graph has been run, opalserve start also spawns a Graphiti Python process as a child. The child speaks MCP stdio to OpalServe, which proxies it under the graph_search tool name out to linked editors.
Communication is one-way at startup (OpalServe → Graphiti via env vars) and bidirectional during normal operation (MCP RPC). On graceful shutdown, OpalServe sends SIGTERM, waits for clean exit, and removes the PID file.
The full Graphiti architecture, sidecar process model, ingestion pipeline, LLM provider configuration, eight implementation milestones, lives at docs/GRAPHITI_ARCHITECTURE.md.
Process model summary
One Node process. One SQLite file. Optionally one Python sidecar. Everything else is a network call.
That's the entire system. There is no Redis, no message queue, no Kafka, no separate worker pool. If you can run Node 20, you can run OpalServe.
Why this shape
Three constraints drove every choice:
- Self-hostable on a Raspberry Pi. No external service dependencies allowed. SQLite over Postgres, embedded Kuzu over Neo4j, one process over a fleet.
- MIT licensed, no telemetry. Every line of code is auditable. Nothing phones home. Storage is a file you own.
- Production-grade where it matters. HMAC-hashed keys, helmet headers, zod validation, audit log, rate limits, even on a Raspberry Pi install.
Every architectural choice is testable against those three. When the answer is "yes to all three," the choice ships.