Architecture
Data flow
Agent → HTTP (MCP JSON-RPC) → klaxon-server (axum + PostgreSQL)
↓
REST + WebSocket → Web dashboard
REST + WebSocket → Mobile (Expo)
↓
FCM push → mobile + Web Push → browser
SSE (MCP spec) ← Agent (subscribed)
Browser / Mobile → /auth/*, /oauth/* → klaxon-auth (OAuth 2.1 AS)
↓
GitHub · Google · Apple · magic linkIn production, a single Ingress path-routes /auth/*, /oauth/*, and /.well-known/oauth-authorization-server to the auth pod; everything else (including /.well-known/oauth-protected-resource) goes to the server pod. Both binaries share one Postgres database. A W3C traceparent header propagates browser → auth → server so one OpenTelemetry trace spans the whole stack.
Rust crates
The Cargo workspace has four crates:
crates/klaxon-server/ — resource server
The primary binary. REST handlers, the MCP JSON-RPC endpoint, WebSocket fan-out, the push/webhook workers, and rate limiting. Runs on port 3000 by default. A --worker flag re-executes the same binary in worker mode for the background job loop.
| Module | Purpose |
|---|---|
main.rs | Server / worker mode dispatch |
config.rs | Typed configuration from env |
state.rs | AppState (PgPool + BillingConfig) |
mcp.rs | JSON-RPC 2.0 dispatch + tool registry + SSE |
ws.rs | WebSocket endpoint — first-message auth because browser WebSocket can't send headers |
handlers/ | REST handlers — items, channels, teams, roles, invitations, admin, billing, webhooks, templates, push, audit, settings, stats |
billing/gate.rs | Thin wrapper around klaxon-auth-core's shared gate |
worker.rs | Snooze / archive / push queue / webhook delivery / email / cleanup |
metrics.rs | Prometheus middleware |
rate_limit.rs | Redis-backed sliding window; Postgres fallback |
crates/klaxon-auth/ — OAuth 2.1 Authorization Server
Runs on port 3001. RFC 8414 metadata discovery, RFC 7591 dynamic client registration, PKCE S256, refresh-token rotation, RFC 7009 revocation. Handles IdP login for GitHub, Google, Apple (ES256 JWT client_secret + JWKS-verified id_token), and magic link. First-time signup auto-creates a personal org + user + identity row. Owns the invitation handler (both admin-side create and public-facing accept).
crates/klaxon-auth-core/ — shared extractors
Consumed by both binaries. Provides the AuthUser axum extractor that resolves bearer tokens against three Postgres tables (sessions, api_keys, oauth_tokens). Also holds the permission string matcher (has_permission), the billing gate (check_limit), PKCE S256 verification, and set_resource_metadata_url for the RFC 9728 WWW-Authenticate header. Generic over any state S: FromRef<PgPool>.
crates/klaxon-telemetry/ — shared OTel init
One function: init(cfg) -> TelemetryGuard that wires traces (OTLP/gRPC), logs (opentelemetry-appender-tracing with trace_id correlation), and W3C propagators. The guard owns the providers; Drop flushes. Layer ordering is load-bearing — the trace layer must install before the log appender or log records lose trace_id.
Database
PostgreSQL 16+ with 20 migrations — one shared schema per deployment, queried by both binaries. Every tenant-scoped table carries org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE. Migrations live in crates/klaxon-server/migrations/ and run at server startup (whichever binary starts first wins the race; the migration system is idempotent).
Key design choices:
- UUID primary keys with
gen_random_uuid() - TIMESTAMPTZ for all timestamps (not text)
- JSONB for form schemas, actions, responses, metadata
- TEXT[] for tags, UUID[] for related items
- Postgres ENUM types for
item_level,item_status,actor_type,channel_mode - Cursor-based pagination via compound
(org_id, created_at DESC, id DESC)indexes - LISTEN/NOTIFY on
klaxon_eventschannel for multi-replica event broadcast
See Database for the full schema.
Client Packages
| Package | Name | Purpose |
|---|---|---|
packages/protocol/ | @ottercoders/klaxon-protocol | Zod schemas — the TypeScript contract |
packages/common/ | @ottercoders/klaxon-common | Shared components, widgets, hooks, API transport |
packages/ui/ | @klaxon/ui | Desktop client entry point |
packages/web/ | @klaxon/web | Browser web app |
apps/klaxon-mobile/ | klaxon-mobile | Expo React Native mobile app |
Shared transport layer
packages/common/src/api.ts provides invoke() and listen() abstractions. invoke() maps command names to REST API calls (with OpenTelemetry-instrumented fetch), and listen() is a local event emitter fed by a WebSocket connection. All widgets work identically across web and mobile (with React Native adaptations) — the transport is swapped, not the UI code.
Multi-Tenancy
Every API request is scoped to an organization (org_id). The AuthUser extractor resolves the bearer token to (user_id, org_id, agent_id) and every database query filters by org_id.
Channel membership controls item visibility: users only see items in channels they belong to (decision D2 from the migration questionnaire).