λx.xDocs← app

Architecture

An engineering deep-dive into how Vinculum is built — for engineers evaluating the system or wanting to understand its internals before contributing.

Stack overview

LayerTechnology
MCP server + REST APIPython 3.12, FastMCP 3.2.2 (Starlette), port 31415
DatabasePostgreSQL 17 + pgvector extension
Dashboard frontendNext.js 16, React 19, Tailwind v4, TypeScript
Real-time pushPostgreSQL LISTEN/NOTIFY → FastAPI SSE → React
Semantic searchVoyage AI embeddings stored as pgvector columns
AI intelligenceAnthropic Haiku (delta classification, auto-titles)
The MetricStrip (mission control) shows branch health, active sessions, and entry velocity — this is the real component from the dashboard, fed static demo data.

Two processes run in production: the Python MCP server (with embedded REST API) and the Next.js frontend. In the Docker Compose setup they share a network; the frontend proxies /api/* and /mcp to the Python server.

Database schema

All tables live in the vinculum schema. PostgreSQL is the coordination substrate — sessions don't share a chat thread, they share the database. See Concepts → Substrate for the higher-level framing.

Core tables

vinculum.entries — the primary data structure. Every decision, spec, note, question, implementation, and checkpoint is a typed entry.

ColumnTypeNotes
idbigintInternal global sequential ID — never shown to users
seq_nobigintUser-facing per-project entry number (the #42 on the dashboard)
uuiduuidStable external reference — used for cross-entry tool args
project_idtextMulti-tenant key (default: vinculum)
branchtextOne of 6 branches per project
thread_slugtextGroups entries into threads
entry_typetextdecision, spec, note, question, implementation, …
contenttextFull markdown content
metadatajsonbStructured payload: target, priority, links, acceptance criteria
tsvtsvectorGenerated column for full-text search
embeddingvector(512)Voyage voyage-3-lite embedding (512-dim) for semantic similarity search
deltaintFocus-distance classification: 0 = on-focus, 3 = off-topic
authortextSession label of the writing session
superseded_bybigintPoints to the replacement entry

vinculum.threads — groups entries. Composite PK (project_id, slug). Status: open, blocked, closed.

vinculum.links — directed graph edges between entries. Relation is one of: supersedes, references, blocks, implements, contradicts, replies_to.

vinculum.sessions — connected AI clients. Tracks focus (branch/thread), display color, declared focus label, last activity.

vinculum.projects — multi-tenant. Default project is vinculum with 6 branches: growth, platform, product, design, content, factory.

vinculum.attention_items — items surfaced to the human. Severity: info, warning, critical.

vinculum.audit_log — every MCP tool invocation with args and timing. vinculum.media — uploaded images stored as BYTEA.

Spawner tables

vinculum.spawn_requests — the spawn state machine. Status transitions: pending → claimed → running → completed / failed. Contains spawn_uuid, directive_id, host, tmux_target, and a rich metadata payload.

vinculum.spawn_log — one row per spawned grunt with tmux_target, session_id, role, directive_id. Indexed for claim_spawn lookups.

vinculum.trust_profiles — role-keyed allow/deny pattern lists. Seeds 4 defaults at install time: colonel (full), builder (constrained shell), historian (read-only), critic (review-only).

vinculum.queued_prompts — prompts queued for sequential delivery into an active grunt session.

Spawner architecture

Spawning is runner-only. Every spawn — grunt, lieutenant, batch-review — enqueues a spawn_request row that a paired vinculum-runner claims and executes. There is no in-process Popen path, no host-shim, and no VINCULUM_SPAWN_MODE / VINCULUM_ALLOW_LOCAL_SPAWN knobs. See also Concepts → Spawning.

text
spawn_grunt tool call
  → server writes spawn_requests row (status=pending)
  → vinculum-runner (Go binary on your machine) long-polls GET /runner/poll
  → runner claims the row (SKIP LOCKED), launches claude in a new tmux window
  → runner writes shell_pid + tmux_target back; status → claimed
  → grunt calls claim_spawn as first tool call

The runner makes only outbound HTTPS connections — no inbound ports, no VPN, no database connection. It claims only the spawn_request rows targeted at its own runner_id, so multiple runners never race. The native installers register it to start at login (systemd user unit on Linux, launchd agent on macOS, Scheduled Task on Windows). No runner container image is published.

Signing in, not installing a daemon

Signing in on a machine connects its runner — in the Vinculum app, or with vinculum-runner login on a headless box. Nothing to copy between machines and no install-spawnd.sh step; the runner polls the server outbound.

Server-side reconciliation

spawn_log.status reconciliation runs server-side as the spawn_reconcile watchdog tick (db.reconcile_spawn_log), not as a host-side script or timer. The runner writes a terminal status on grunt exit; the tick is the safety net for grunts that vanish silently.

MCP intelligence layer

Vinculum's AI features use a sampling-via-client model: the MCP server never directly calls the Anthropic API on its own key. Instead, it uses the MCP sampling capability — sending sampling/createMessage requests back to the connected AI client, which routes inference through the client's API key. This is the structural cost advantage: the server has no inference bill.

Delta classification

On every write call, the server classifies the entry's semantic distance from the session's declared focus (0 = on-focus, 3 = off-topic). Runs fire-and-forget via asyncio.create_task. Falls back to branch/thread heuristic when VINCULUM_ANTHROPIC_API_KEY is unset.

Auto-titles

The title engine listens on the PostgreSQL vinculum_title_regen channel. When a thread gets new entries, it debounces for 30 seconds then generates a descriptive title via Haiku (or heuristic fallback). Thread lists stay readable without manual title management.

Semantic search (optional)

When VINCULUM_VOYAGE_API_KEY is set, each new entry gets a Voyage AI embedding stored in the embedding column. The search MCP tool and /api/dashboard/semantic-related endpoint use pgvector cosine similarity to find conceptually related entries across threads and branches.

Real-time pipeline

text
write() tool call
  → SQL INSERT into vinculum.entries
  → PostgreSQL trigger fires NOTIFY vinculum_new_entry, payload=entry_id
  → Starlette SSE generator receives notification (asyncio + psycopg3 async)
  → sends delta event to all connected /api/dashboard/stream clients
  → React dashboard patches branch/thread/entry state (no full reload)

Sub-second latency from write to UI update. The SSE endpoint holds a long-lived HTTP connection per browser tab; PostgreSQL NOTIFY is the push mechanism. No polling, no websockets, no additional infrastructure.

Disable proxy buffering

Reverse proxies must have buffering disabled for SSE to work. See the self-hosting guide for nginx and Caddy config samples.

Trust profile and permission_request flow

Grunts can be gated on tool calls via trust profiles — role-keyed allow/deny pattern lists that control which shell commands and MCP tools each session may invoke without colonel approval:

  1. assign_role sets the session's trust profile (e.g., builder, historian)
  2. Before each tool call, the PreToolUse hook checks the session's allow/deny patterns in vinculum.trust_profiles
  3. If denied: the grunt creates a permission_request entry and blocks via await_peer_response
  4. The colonel approves or denies via approve_grunt_action / deny — the grunt unblocks automatically

Migration 075

Migration 075 auto-resolves stale permission requests from terminal grunts — preventing the colonel's inbox from accumulating ghost requests from sessions that already finished.

Why this scales

The horizontal coordination thesis: the bottleneck in parallel AI work is not per-session intelligence, it is the coordination substrate. A team of ten AI sessions running simultaneously on separate directives is bottlenecked by how they pass state to each other, not by how smart each session is.

Vinculum's answers:

  • Sessions share a database, not a chat thread. Any number of sessions can read and write the substrate concurrently with no serialization cost.
  • Per-turn context injection (~800 tokens) keeps sessions aware of each other without burning full context on coordination.
  • Typed entries with structured metadata (priority, target role, attention flag) let the substrate route work automatically — a blocking question reaches the colonel's inbox without manual forwarding.
  • Auditable work log. Every claim, checkpoint, and implementation entry is preserved. The colonel can reconstruct exactly what was decided and why, months later.