nexus/.planning/codebase/ARCHITECTURE.md
Mikkel Georgsen 3c85784b6d [nexus] chore: migrate .planning/ from agent repo to nexus repo
Planning artifacts (milestones v1.0-v1.2.1, v1.3 queue, PROJECT.md,
STATE.md, config) now live alongside the code they describe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:08:50 +00:00

23 KiB

Architecture

Analysis Date: 2026-03-30 Codebase: Paperclip (nexus repo at /Volumes/UsbNvme/repos/nexus/)


Pattern Overview

Overall: Multi-package TypeScript monorepo. Server-rendered React SPA (Vite) + Express API server + CLI tool + adapter plugin system.

Key Characteristics:

  • pnpm workspaces monorepo with five top-level workspace members: server, ui, cli, packages/*, plugins/*
  • Express REST API with actor-based auth middleware (board users, agents, instance admin)
  • React SPA using React Router v6 with company-prefixed URL scheme (e.g., /PAP/issues)
  • Agent execution driven by a heartbeat lifecycle: wakeup requests → heartbeat runs → adapter subprocess calls
  • Plugin system with isolated worker processes and structured host/worker RPC

Monorepo Structure

nexus/
├── server/              # Express API + business logic
│   ├── src/
│   │   ├── routes/      # Express route handlers
│   │   ├── services/    # Business logic layer
│   │   ├── adapters/    # Agent execution adapter registry
│   │   ├── auth/        # BetterAuth integration
│   │   ├── middleware/  # auth, logging, validation, error handling
│   │   ├── realtime/    # WebSocket live-events bridge
│   │   ├── secrets/     # Secret provider abstraction
│   │   ├── storage/     # S3/local file storage abstraction
│   │   └── app.ts       # Express app factory
├── ui/                  # React SPA (Vite)
│   └── src/
│       ├── pages/       # Top-level page components
│       ├── components/  # Shared UI components
│       ├── context/     # React contexts (company, dialog, live updates)
│       ├── api/         # API client modules (one per domain)
│       ├── hooks/       # Custom React hooks
│       └── App.tsx      # Router + route tree
├── cli/                 # paperclipai CLI (Commander.js)
│   └── src/
│       ├── commands/    # CLI command implementations
│       ├── config/      # Config file read/write
│       └── prompts/     # @clack/prompts interactive wizards
├── packages/
│   ├── db/              # Drizzle ORM schema + migrations + DB client
│   ├── shared/          # Shared types, validators (Zod), constants
│   ├── adapter-utils/   # Shared adapter type contracts + session utils
│   └── adapters/        # Adapter packages (one per AI tool)
│       ├── claude-local/
│       ├── codex-local/
│       ├── cursor-local/
│       ├── gemini-local/
│       ├── opencode-local/
│       ├── pi-local/
│       └── openclaw-gateway/
└── plugins/
    ├── sdk/             # @paperclipai/plugin-sdk (worker-side SDK)
    ├── create-paperclip-plugin/  # Plugin scaffolding tool
    └── examples/        # Example plugins

Layers

Database Layer:

  • Purpose: Schema definitions, migrations, Drizzle ORM client
  • Location: packages/db/src/
  • Contains: Drizzle table definitions in schema/, migration SQL in migrations/, client.ts (embedded Postgres or external URL)
  • Depends on: PostgreSQL (embedded via embedded-postgres or external)
  • Used by: Server services exclusively

Services Layer:

  • Purpose: All business logic; one service function per domain
  • Location: server/src/services/
  • Contains: agentService, companyService, heartbeatService, issueService, pluginLifecycleManager, etc.
  • Pattern: Each service is a factory function taking db: Db and returning an object of async methods. No class instances.
  • Depends on: @paperclipai/db schema tables, other services
  • Used by: Route handlers

Route Layer:

  • Purpose: HTTP request parsing, auth assertions, response shaping
  • Location: server/src/routes/
  • Contains: One file per resource domain (companies, agents, issues, projects, routines, plugins, etc.)
  • Pattern: Each route file exports a factory function fooRoutes(db: Db): Router. Routes call service methods, assert auth via assertBoard / assertCompanyAccess helpers.
  • Depends on: Services layer, middleware

Middleware Layer:

  • Purpose: Cross-cutting request processing
  • Location: server/src/middleware/
  • Files:
    • auth.tsactorMiddleware: resolves req.actor as board user, agent, or none
    • board-mutation-guard.ts — blocks unsafe mutations on read-only mounts
    • private-hostname-guard.ts — hostname allowlist enforcement
    • validate.ts — Zod schema validation helper
    • logger.ts — pino logger
    • error-handler.ts — global Express error handler

Adapter Layer:

  • Purpose: Execute agents via different AI tools (Claude, Codex, Cursor, etc.)
  • Location: server/src/adapters/ (registry + process/http sub-adapters), packages/adapters/ (per-tool implementations)
  • Pattern: Each adapter implements ServerAdapterModule from @paperclipai/adapter-utils. The registry (registry.ts) maps adapter type strings to module instances. heartbeatService calls getServerAdapter(type).execute(...) to run agents.

UI Layer:

  • Purpose: React SPA for the board (human operators)
  • Location: ui/src/
  • API communication: ui/src/api/client.ts — simple fetch wrapper. All domain API modules (e.g., ui/src/api/agents.ts) use this api object. No external HTTP library.
  • State: TanStack Query (@tanstack/react-query) for server state. React Context for cross-component state (company selection, dialogs, live updates, theme).
  • Real-time: SSE from /api/realtime consumed by LiveUpdatesProvider in ui/src/context/LiveUpdatesProvider.tsx.

CLI Layer:

  • Purpose: Operator tooling — first-run setup, diagnostics, server run, one-off commands
  • Location: cli/src/
  • Framework: Commander.js with @clack/prompts for interactive steps

Authentication Model

Two principal types flow through actorMiddleware (server/src/middleware/auth.ts):

Board (human):

  • local_trusted deployment: automatically granted isInstanceAdmin: true, no auth needed
  • authenticated deployment: BetterAuth session cookie OR bearer API key (from board_api_keys table)
  • req.actor.type === "board", carries userId, companyIds[], isInstanceAdmin

Agent:

  • Bearer token: either a hashed API key from agent_api_keys table, or a short-lived JWT (createLocalAgentJwt)
  • req.actor.type === "agent", carries agentId, companyId
  • Agents cannot access other companies; CEO agents get additional mutation rights (branding, portability)

Route authorization helpers live in server/src/routes/authz.ts:

  • assertBoard(req) — throws 403 if not board
  • assertCompanyAccess(req, companyId) — throws 403 if actor cannot access this company
  • assertInstanceAdmin(req) — throws 403 if not instance admin

Company Model

Companies are the top-level tenant container. They map to what will become "projects" in Nexus.

Schema: packages/db/src/schema/companies.ts

companies
  id uuid (PK)
  name text
  description text
  status text  (active | paused | archived)
  issuePrefix text UNIQUE  // e.g. "PAP" — used in URLs and identifiers
  issueCounter integer     // auto-increment for issue numbers
  budgetMonthlyCents integer
  requireBoardApprovalForNewAgents boolean
  brandColor text

Company creation flow (POST /api/companies):

  1. Board asserts instance admin
  2. companyService.create(data) inserts company
  3. accessService.ensureMembership(...) adds creator as owner in company_memberships
  4. If budget set, budgetService.upsertPolicy(...) creates budget policy
  5. Activity logged

Company service: server/src/services/companies.tscompanyService(db) factory returning list, getById, create, update, archive, remove, stats.

Company portability: server/src/services/company-portability.ts — import/export bundles; CEO agents can run imports/exports on their own company.

Company members: packages/db/src/schema/company_memberships.tsprincipalType (user|agent), principalId, companyId, role (owner|member), status (active|invited|etc.)


Agent Model

Agents are AI workers belonging to a company. CEO is a special role with elevated permissions.

Schema: packages/db/src/schema/agents.ts

agents
  id uuid (PK)
  companyId uuid (FK companies)
  name text
  role text         // "ceo" | "general" | custom
  title text
  reportsTo uuid    // self-referential FK for org chart
  status text       // idle | running | paused | terminated | pending_approval
  adapterType text  // "claude_local" | "codex_local" | "cursor" | etc.
  adapterConfig jsonb   // adapter-specific config (model, instructionsFilePath, etc.)
  runtimeConfig jsonb   // session compaction policy, max concurrent runs
  budgetMonthlyCents integer
  permissions jsonb     // canCreateAgents, etc.
  lastHeartbeatAt timestamp

Agent service: server/src/services/agents.tsagentService(db) factory. Key methods:

  • create, update, pause, resume, terminate, remove
  • orgForCompany(companyId) — builds hierarchical org tree from reportsTo links
  • getChainOfCommand(agentId) — walks up reportsTo chain
  • createApiKey, listKeys, revokeKey
  • rollbackConfigRevision — restores an agent config from agent_config_revisions

CEO agents: Identified by role === "ceo". Only CEO agents can:

  • Update company branding (PATCH /api/companies/:id/branding)
  • Manage company portability (import/export)
  • The default first task in onboarding is assigned to the CEO agent

Agent Heartbeat / Task Lifecycle

This is the core execution engine. An "agent run" is called a heartbeat run.

Flow:

1. Wake request queued
   → agent_wakeup_requests (status: queued)
   → agentWakeupRequest.source = "timer" | "assignment" | "on_demand" | "automation"

2. heartbeatService.runHeartbeat(agentId, options)
   → acquires per-agent start lock (Map<agentId, Promise>)
   → checks concurrent run limits (runtimeConfig.maxConcurrentRuns, default 1)
   → resolves execution workspace (project repo, task session, or agent home dir)
   → resolves session state (previous session params from agent_task_sessions)
   → checks session compaction (rotate if maxSessionRuns / maxRawInputTokens exceeded)
   → inserts heartbeat_runs row (status: queued → running)

3. getServerAdapter(adapterType).execute(context, meta)
   → adapter launches subprocess or HTTP call
   → streams stdout/stderr chunks back
   → heartbeatService writes live log chunks to run log store

4. On completion:
   → updates heartbeat_runs (status: done | error | cancelled)
   → persists session state to agent_task_sessions
   → writes cost event to cost_events
   → publishes live event to EventEmitter for SSE clients
   → updates issue status if issue was being worked

5. Wakeup request marked finished

Key tables:

  • heartbeat_runs — one row per execution (packages/db/src/schema/heartbeat_runs.ts)
  • agent_wakeup_requests — queued wakeup requests, coalesced by idempotency key
  • agent_task_sessions — persisted session params per (agent, adapterType, taskKey)
  • agent_runtime_state — current session ID for an agent

heartbeatService: server/src/services/heartbeat.ts (~1400 lines). The central orchestrator. Dependencies: adapter registry, workspace services, session codec, budget service, issue service, cost service.


Adapter System

Adapters wrap specific AI tools (Claude Code, OpenAI Codex, Cursor, Gemini, etc.).

Contract: ServerAdapterModule from packages/adapter-utils/. Key methods:

  • execute(context, meta): Promise<AdapterExecutionResult> — run the agent
  • testEnvironment(config): Promise<AdapterEnvironmentTestResult> — check prerequisites
  • listSkills/syncSkills — skill management
  • sessionCodec — serialize/deserialize session state
  • onHireApproved (optional) — notify adapter when a hired agent is approved

Registry: server/src/adapters/registry.ts — maps type string to ServerAdapterModule. Falls back to processAdapter for unknown types.

Registered adapter types:

  • claude_local — Claude Code CLI subprocess (packages/adapters/claude-local/)
  • codex_local — OpenAI Codex (packages/adapters/codex-local/)
  • opencode_local — OpenCode (packages/adapters/opencode-local/)
  • pi_local — Pi adapter (packages/adapters/pi-local/)
  • cursor — Cursor IDE (packages/adapters/cursor-local/)
  • gemini_local — Gemini CLI (packages/adapters/gemini-local/)
  • openclaw_gateway — HTTP gateway adapter (packages/adapters/openclaw-gateway/)
  • process — generic subprocess adapter (server/src/adapters/process/)
  • http — generic HTTP adapter (server/src/adapters/http/)
  • hermes_local — third-party Hermes adapter

Process adapter: server/src/adapters/process/ — launches a configured command as a child process, streams stdout/stderr, parses structured JSON event chunks from the process.


Plugin System

Plugins extend Paperclip with custom tools, UI slots, scheduled jobs, and webhooks.

Architecture:

  • Each plugin runs as a separate worker process (isolated via pluginWorkerManager)
  • Host (server) communicates with worker via JSON-RPC over stdin/stdout
  • Host provides PluginContext services to the worker via the SDK

Key services:

  • plugin-lifecycle.ts — state machine: installed → ready → disabled/error/upgrade_pending → uninstalled. Starting/stopping worker processes on transitions.
  • plugin-loader.ts — loads plugin packages from disk, validates manifests
  • plugin-worker-manager.ts — spawns/manages worker processes
  • plugin-job-coordinator.ts + plugin-job-scheduler.ts — scheduled job execution
  • plugin-tool-dispatcher.ts — routes tool invocations from agents to plugin workers
  • plugin-event-bus.ts — dispatches Paperclip events (issue.created, agent.run.completed, etc.) to plugin workers
  • plugin-host-services.ts — host-side service implementations exposed to workers via SDK
  • plugin-registry.ts — DB CRUD for plugin records

Worker SDK: packages/plugins/sdk/src/@paperclipai/plugin-sdk. Plugin authors import this to define tools, jobs, webhooks, UI slots, and interact with Paperclip state.

Plugin manifest: PaperclipPluginManifestV1 from @paperclipai/shared — declares capabilities, tools, jobs, webhooks, UI slots.

DB tables: plugins, plugin_config, plugin_state, plugin_jobs, plugin_logs, plugin_entities, plugin_webhooks, plugin_company_settings


Issue / Task Model

Issues are work items assigned to agents or users.

Schema: packages/db/src/schema/issues.ts — key fields:

issues
  id, companyId, projectId, goalId, parentId (self-ref)
  title, description, status (backlog|todo|in_progress|in_review|blocked|done|cancelled)
  priority (low|medium|high|urgent)
  assigneeAgentId, assigneeUserId
  checkoutRunId, executionRunId  // FK to heartbeat_runs
  executionWorkspaceId
  issueNumber, identifier (e.g. "PAP-42")
  originKind, originId  // "manual" | "routine_execution" | "heartbeat"
  requestDepth  // for sub-issues created by agents

Identifier format: {issuePrefix}-{issueNumber} — e.g., PAP-42. The prefix is unique per company.

Sub-issues: parentId self-reference allows agent-created sub-issues. requestDepth tracks nesting.

Execution workspace: Each issue can have an executionWorkspaceId pointing to a specific git worktree or container workspace. Managed by execution-workspace-policy.ts.


Project Model

Projects group issues and have an optional lead agent and git repo.

Schema: packages/db/src/schema/projects.ts

projects
  id, companyId, goalId
  name, description, status (backlog|active|paused|done|archived)
  leadAgentId
  targetDate, color
  executionWorkspacePolicy jsonb  // repo URL, worktree settings

Projects will become the renamed entity in Nexus (replacing "companies" as the primary project container).


Goal Model

Goals are objectives that group projects and issues.

Schema: packages/db/src/schema/goals.tsid, companyId, title, description, status, parentId (hierarchical)


Onboarding Flow

CLI Onboarding (paperclipai onboard)

Interactive wizard in cli/src/commands/onboard.ts driven by @clack/prompts:

  1. Mode selection — "quickstart" (embedded Postgres, sane defaults) or "advanced" (custom DB, S3, auth)
  2. Database — embedded Postgres (auto-configured) or external DATABASE_URL
  3. LLM — configure default LLM provider/API key
  4. Server — port, host, deployment mode (local_trusted vs authenticated), public URL
  5. Storage — local filesystem or S3
  6. Secrets — local encrypted file or external secret manager
  7. Writes config to ~/.paperclip/config.yaml (or --config path)
  8. Optionally runs auth bootstrap-ceo to generate first admin invite

UI Onboarding Wizard (OnboardingWizard.tsx)

4-step dialog at ui/src/components/OnboardingWizard.tsx:

  • Step 1 — Company: enter company name + high-level goal. Creates company via POST /api/companies, creates a Goal record.
  • Step 2 — Agent: name the CEO agent, select adapter type (claude_local, codex_local, cursor, etc.), configure model. Tests adapter environment. Creates agent via POST /api/companies/:id/agents.
  • Step 3 — First Task: pre-filled task description ("Hire a founding engineer, write a hiring plan, break the roadmap into concrete tasks"). Creates a project + issue assigned to the CEO agent.
  • Step 4 — Launch: confirms entities created, navigates to issue detail. Shows run transcript.

Trigger: openOnboarding() from DialogContext. Also triggered by route /onboarding or by navigating to company-prefixed routes when no companies exist.

Default task: The CEO agent's first issue is "Hire your first engineer and create a hiring plan" with the task description guiding it to hire a founding engineer and begin delegating work.


Real-time Updates

Server: server/src/services/live-events.ts — in-process EventEmitter. Services call publishLiveEvent({ companyId, type, payload }) after mutations. SSE endpoint in server/src/realtime/live-events-ws.ts streams events per company.

Client: ui/src/context/LiveUpdatesProvider.tsx — subscribes to SSE, calls queryClient.invalidateQueries(...) to refresh TanStack Query caches on relevant events. Also shows toast notifications for agent run completions and issue status changes.


CLI Commands

Entry point: cli/src/index.ts — Commander.js program named paperclipai.

Setup commands:

  • onboard — interactive first-run setup wizard
  • run — onboard + doctor + start server
  • configure — update config sections (llm, database, logging, server, storage, secrets)
  • doctor — diagnostic checks + optional auto-repair
  • env — print deployment environment variables
  • allowed-hostname — add hostname to allowlist
  • db:backup — one-off database backup

Runtime commands:

  • heartbeat run --agent-id <id> — trigger one agent heartbeat and stream logs

Client commands (interact with a running Paperclip instance):

  • company list|create|get — CRUD on companies
  • issue list|create|get|update — CRUD on issues
  • agent list|create|get|update — CRUD on agents
  • approval list|get|approve|reject — manage approvals
  • activity list — view activity log
  • dashboard — display dashboard stats
  • plugin list|install|enable|disable|uninstall — plugin management
  • auth login|logout|whoami|bootstrap-ceo — auth operations
  • worktree ... — git worktree management for execution workspaces

Config/context for client commands loaded from ~/.paperclip/config.yaml or --config flag.


Routine Model

Routines are scheduled triggers that create issues on a cron-like cadence.

Schema: packages/db/src/schema/routines.ts

Routines fire at configured intervals, create issues with the configured title/description, and assign them to the specified agent. server/src/services/cron.ts drives the scheduling.


Execution Workspace Model

Execution workspaces provide isolated git checkouts per project or issue.

Schema: packages/db/src/schema/execution_workspaces.ts, project_workspaces.ts

Managed by server/src/services/execution-workspace-policy.ts and workspace-runtime.ts. On heartbeat start, heartbeatService calls realizeExecutionWorkspace(...) to ensure the workspace directory + git clone exist before passing cwd to the adapter.


Key Data Model Relationships

Instance
  └── companies (1:N)
        ├── agents (1:N) — role: ceo | general | custom
        │     └── reportsTo (self-ref tree)
        ├── projects (1:N)
        │     └── issues (1:N)
        ├── goals (1:N, hierarchical)
        ├── routines (1:N)
        ├── heartbeat_runs (1:N, via agents)
        │     └── heartbeat_run_events (1:N)
        ├── agent_wakeup_requests (1:N)
        ├── agent_task_sessions (1:N)
        ├── approvals (1:N)
        ├── cost_events (1:N)
        ├── company_secrets (1:N)
        └── plugins (M:N via plugin_company_settings)

Entry Points

Server:

  • server/src/index.ts — boots Postgres, runs migrations, calls createApp(db, opts), starts HTTP listener
  • server/src/app.tscreateApp(db, opts) — wires Express middleware, mounts all routes, initializes plugin system, returns app

UI:

  • ui/src/main.tsx — React root, wraps <App /> in TanStack Query provider, CompanyProvider, etc.
  • ui/src/App.tsx — top-level router with CloudAccessGate, company-prefixed routes, <OnboardingWizard />

CLI:

  • cli/src/index.ts — Commander program, registers all commands, calls program.parseAsync()

Error Handling

Server: server/src/errors.ts exports notFound, conflict, forbidden, unprocessable helpers that throw typed errors. server/src/middleware/error-handler.ts catches them and returns structured JSON: { error: string }.

UI: ui/src/api/client.tsApiError class with status and body. Components check error instanceof ApiError and display error.message.


Deployment Modes

Controlled by PAPERCLIP_DEPLOYMENT_MODE:

  • local_trusted — single-user local, no auth required. All requests automatically get isInstanceAdmin: true.
  • authenticated — multi-user, requires BetterAuth session or API key. Board API keys stored in board_api_keys.

PAPERCLIP_DEPLOYMENT_EXPOSURE:

  • public — open to external traffic
  • private — private-hostname-guard enforces allowlist

Architecture analysis: 2026-03-30