cambium ▸ docs/specs/spec-tech-stack.md
updated 2026-03-27

Spec — Tech Stack

Document Status

Draft V1

Purpose

This document defines the technology choices for Cambium V1.

The priorities are: 1. Ship a working modular monolith fast 2. Don’t paint into corners that prevent scaling later 3. Align with the ecosystem Ralph operates in 4. Keep the stack boring where it doesn’t matter and intentional where it does


Decision: Language

Backend: TypeScript (Node.js)

Why: - Aligns with OpenClaw ecosystem (Ralph’s native environment) - Strong type system for enforcing domain contracts - Excellent async support for event-driven architecture - Huge ecosystem for integrations (LLM providers, APIs, databases) - Fast iteration in modular monolith phase - Same language for backend and frontend reduces context switching

Runtime: Node.js LTS (v22+)

Strict mode: TypeScript strict mode enabled from day one. The signal protocol and agent model contracts are too important for any to creep in.


Decision: Persistence

Primary Database: PostgreSQL

Why: - Battle-tested for durable state (the core requirement) - JSONB columns for flexible payloads (event payloads, agent configs, workflow graph specs) - Strong transaction support for state machine transitions - LISTEN/NOTIFY for lightweight event propagation within the monolith - Full-text search for event/log querying - Excellent migration tooling - Scales well beyond V1 needs

Schema approach: - Structured columns for core fields (IDs, statuses, timestamps, foreign keys) - JSONB for flexible payload fields (graph specs, signal payloads, agent configs) - This avoids the rigidity of fully normalized schemas while keeping queryable structure

ORM / Query Layer: Drizzle ORM

Why: - TypeScript-native, type-safe queries - Lightweight — doesn’t hide SQL behind abstractions - Excellent migration support - Good JSONB handling - Fits the “boring where it doesn’t matter” principle

Alternative considered: SQLite


Decision: Event Infrastructure

V1: In-Process Event Bus

Why: - Modular monolith = single process, no need for external message broker - Simpler debugging — events are in-process function calls with persistence - Faster iteration — no Kafka/NATS/Redis cluster to manage - The signal protocol is the abstraction layer — if we need to swap to external messaging later, we replace the bus implementation, not the signal contracts

Implementation: - TypeScript EventEmitter-based bus with typed events - Every signal persisted to PostgreSQL BEFORE delivery to subscribers - At-least-once delivery guarantee within the process - Failed handler retries with exponential backoff - Dead-letter table for persistently failed deliveries

V2 Migration Path

When scale requires it, the bus implementation swaps to: - Redis Streams for multi-process coordination - NATS if we need clustering - PostgreSQL LISTEN/NOTIFY as an intermediate step

The signal protocol contracts don’t change. Only the transport does.


Decision: Frontend (Loom)

Framework: Next.js (App Router) + React

Why: - Server-side rendering for initial load performance - API routes co-located with UI (simplifies monolith deployment) - React ecosystem for graph visualization (React Flow for workflow editor) - Strong TypeScript support - Shared types between backend and frontend in monorepo

Key Libraries

Real-Time Updates


Decision: Project Structure

Monorepo with Turborepo

cambium/
├── apps/
│   └── loom/                    # Next.js frontend
├── packages/
│   ├── core/                    # Shared types, contracts, identity, permissions
│   ├── hypha/                   # Orchestration engine
│   ├── quorum/                  # Governance and threshold decisions
│   ├── health/                  # Homeostasis and stress detection
│   ├── seed/                    # Checkpoints, replay, recovery
│   ├── immune/                  # Anomaly detection and trust
│   ├── stigmergy/               # Shared environment and markers
│   ├── genesis/                 # Agent templates and lifecycle
│   ├── signals/                 # Signal protocol, bus, receptor registry
│   ├── events/                  # Event schemas and persistence
│   ├── audit/                   # Audit trail
│   ├── agents/                  # Agent runtime, registration, heartbeat
│   ├── workflows/               # Workflow definition schema, compiler
│   ├── integrations/            # Tool bindings, LLM providers, external connectors
│   └── db/                      # Database schema, migrations, query helpers
├── infrastructure/
│   ├── docker/
│   └── scripts/
├── docs/
│   ├── architecture/
│   ├── prd/
│   └── specs/
├── turbo.json
├── package.json
└── tsconfig.base.json

Package Boundaries


Decision: API Layer

Internal: Direct Function Calls

Within the modular monolith, packages call each other through typed function interfaces. No HTTP between modules.

External: REST API + SSE

Future: GraphQL or tRPC

If the API surface grows complex, consider tRPC (type-safe RPC between Next.js and backend). Not needed for V1.


Decision: Testing

Strategy

Coverage Priority

  1. Signal cascade chains (critical safety paths)
  2. Workflow state machine transitions
  3. Quorum decision logic
  4. Seed checkpoint/recovery
  5. Agent lifecycle
  6. Loom interaction flows

Decision: Deployment (V1)

Local Development

Production (V1)

Future


Decision: Development Tooling


Constraints and Non-Negotiables

  1. TypeScript strict mode everywhere — the signal protocol and agent contracts are safety-critical
  2. PostgreSQL for state — no in-memory-only state for anything important
  3. Signals persisted before delivery — no fire-and-forget for the nervous system
  4. Package boundaries enforced — if the monolith’s modules are tangled, splitting later becomes surgery
  5. Tests for cascade chains — untested safety paths are not safety paths

Open Questions

  1. Should we use a schema validation library (Zod) for runtime signal validation, or rely on TypeScript types alone?
  2. Do we need a job queue (BullMQ) for task dispatch, or is the in-process model sufficient for V1?
  3. Should the workflow graph spec use a standard format (e.g., JSON-based DAG) or a custom DSL?
  4. What monitoring/observability tooling for V1? (OpenTelemetry? Simple structured logging?)