Skip to Content
Architecture

Architecture

One definition, many surfaces

A dendra procedure is a small, immutable spec: an optional input schema, an optional output schema, middleware, an HTTP method/path (optional), and a handler. From that one spec, dendra projects every surface — they can’t drift, because they all derive from the same definition.

dendra framework architecture: one definition branches into a typed client, REST + OpenAPI, an in-process caller, streaming, LLM tools, and an MCP server

▶ Open the interactive framework diagram

SurfaceProduced by
Typed client (no codegen)createClient<AppRouter>() — inference over typeof appRouter
In-process callercreateCaller(appRouter, ctx)
REST + OpenAPI 3.1toRestHandler, toOpenApiDocument
RPC over HTTPtoNextHandler, toFetchHandler, toNodeHandler
Streaming (NDJSON) & WebSocketroute.stream(...) + the adapters
LLM tool.ai() + toToolset / createToolExecutor
MCP servermcpToolDefinitions / createMcpBridge

Change the input schema and the client type, the REST validation, the OpenAPI entry, and the tool schema all change together.

A real app

The example app wires dendra into a multi-tenant Next.js notes app: the browser and Claude both talk to the same appRouter, which uses SQLite, pub/sub, runAgent, and an MCP connection registry with OAuth.

dendra example app architecture: browser and Claude talk to Next.js routes and the appRouter, which uses SQLite, pub/sub, runAgent, and an MCP connection registry with OAuth

▶ Open the interactive example diagram

The framework ↔ app boundary

Would every dendra user want this regardless of their DB, auth, UI, or which servers they connect to? Yes → framework. No → app.

The app owns, and dendra never touches: authentication, UI/navigation, voice, and MCP connection management (transports, a connection registry, OAuth to remote servers). dendra provides only the typed-procedure and tool-shaping primitives — which is why the core stays zero-dependency.

Validation as the guardrail

Input validation runs before every handler — over HTTP, in-process, and when the model calls a tool. The same schema that rejects a bad REST body rejects a bad tool call, so model output is guarded by the same contract as everything else.

Context

createContext(req) builds the per-request ctx (auth, tenant, db, pubsub, memory). Handlers read ctx; middleware (.use()) refines it. Swap the HTTP adapter for a WebSocket or test adapter and tenancy/auth still work, because every adapter builds ctx the same way.

Last updated on