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
- Simpler to start but insufficient for concurrent workflow execution
- No LISTEN/NOTIFY equivalent
- Connection concurrency limitations
- Would need replacement quickly — not worth the migration tax
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
- React Flow — workflow graph editor and run visualization
- Tailwind CSS — utility-first styling, fast iteration
- Radix UI or shadcn/ui — accessible component primitives
- TanStack Query — server state management
- Recharts or Visx — health metrics visualization
Real-Time Updates
- Server-Sent Events (SSE) for run status and signal updates to Loom
- WebSockets if SSE proves insufficient for V1 interaction patterns
- No polling — the event-driven architecture should push to the UI
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
- Each package has its own
package.jsonandtsconfig.json - Packages import from each other through explicit exports
- No circular dependencies (enforced by tooling)
coreandsignalsare dependency roots — most other packages depend on themhyphadepends onsignals,agents,workflows,stigmergyloomdepends on API contracts from all packages but not implementations
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
- REST API for Loom ↔ backend communication
- REST API for external agent registration
- SSE for real-time signal/event streaming to Loom
- OpenAPI spec generated from TypeScript types
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
- Unit tests: Vitest (fast, TypeScript-native)
- Integration tests: Vitest + test database (PostgreSQL via testcontainers or local)
- E2E tests: Playwright for Loom
- Signal protocol tests: dedicated test harness that validates cascade chains
Coverage Priority
- Signal cascade chains (critical safety paths)
- Workflow state machine transitions
- Quorum decision logic
- Seed checkpoint/recovery
- Agent lifecycle
- Loom interaction flows
Decision: Deployment (V1)
Local Development
docker composefor PostgreSQLturbo devfor all packages + Loom- Single process, all modules in-process
Production (V1)
- Single Node.js process (modular monolith)
- PostgreSQL (managed — Supabase, Neon, or Railway)
- Next.js serving Loom
- Deploy as single container or platform service
Future
- Split modules into services when scale demands
- Signal bus → external message broker
- Horizontal scaling of Hypha workers
- Separate Loom deployment
Decision: Development Tooling
- Package manager: pnpm (workspace support, fast, disk efficient)
- Build: Turborepo (monorepo orchestration, caching)
- Linting: ESLint + Prettier
- Type checking: TypeScript strict mode
- Git hooks: Husky + lint-staged
- CI: GitHub Actions (lint, type-check, test)
Constraints and Non-Negotiables
- TypeScript strict mode everywhere — the signal protocol and agent contracts are safety-critical
- PostgreSQL for state — no in-memory-only state for anything important
- Signals persisted before delivery — no fire-and-forget for the nervous system
- Package boundaries enforced — if the monolith’s modules are tangled, splitting later becomes surgery
- Tests for cascade chains — untested safety paths are not safety paths
Open Questions
- Should we use a schema validation library (Zod) for runtime signal validation, or rely on TypeScript types alone?
- Do we need a job queue (BullMQ) for task dispatch, or is the in-process model sufficient for V1?
- Should the workflow graph spec use a standard format (e.g., JSON-based DAG) or a custom DSL?
- What monitoring/observability tooling for V1? (OpenTelemetry? Simple structured logging?)