Skip to content

Architecture

Control Center follows feature-first Clean Architecture with a ports and adapters pattern. The codebase is organized around business domains (features), with strict dependency rules enforced by an automated architecture test.

Control Center is a thin-client architecture. No client opens the database — a headless cc_server process owns the data (a Drift/SQLite file) and serves it over WebSocket RPC. The desktop app, the web build, and the phone companion are all renderers over that one RPC connection.

The desktop chooses how it reaches its server on first launch: local (it spawns a cc_server on this machine and talks over loopback — the default) or remote (it dials a cc_server elsewhere over a secure WebSocket). The web client is always remote. The boot resolver (bootstrap/server_backend.dart) picks the path before Riverpod exists, then overrides rpcClientProvider with the connected client — so every feature reads and writes through the server rather than a local database.

This is a single resolved-lockfile Dart workspace of eighteen members (plus the root app): the desktop+web app (control_center), the headless server binary (cc_server), the headless fleet executor (cc_worker), the phone PWA (cc_remote), the WebRTC signaling broker (cc_signaling_server), the Widgetbook gallery (cc_gallery), plus shared packages — cc_ui (design system), cc_domain (pure-Dart shared kernel: entities, value objects, ports, events, and every feature’s domain layer), cc_rpc (client transports), cc_host (server kernel), cc_data (remote repositories), cc_persistence (server-side DB), cc_server_core (app-server composition + identity/presence/fleet/evals runtime), cc_infra (VM-only adapters), cc_mcp (MCP tools), cc_markdown (in-repo markdown engine), cc_natives (FFI leaf), and system_audio_capture. See Deployment and clients for the full client matrix and what runs where.

Presentation → Application/Providers → Domain ← Infrastructure
  • Domain layer: pure Dart entities, value objects, repository interfaces, ports, and domain services. Zero infrastructure imports (no dio, drift, or network models).
  • Presentation layer: screens, widgets, notifiers. No direct drift/DAO/data-layer access; everything goes through Riverpod providers → repositories.
  • Infrastructure layer: concrete implementations of domain ports and repository interfaces. Depends on domain, never the other way around.

Each feature follows this structure:

feature_name/
├── data/ # Repository implementations, data sources, services, DTOs, mappers
├── domain/ # Entities, repository interfaces, ports, use cases
├── presentation/ # Screens, widgets, notifiers
└── providers/ # Riverpod providers

Two deliberate exceptions:

  • mcp, orchestration, and plan_studio use application/ instead of presentation/, because their proposal/plan-edit/tool logic is use-case code invoked by external clients or the server rather than UI screens.
  • ticketing and newsfeed add mcp_tools/ for tool metadata that binds their client presentation to the shared MCP registry.

core/domain/ holds entities and repositories shared across 3+ features:

  • Core entities: Agent, AgentRunLog, Workspace, Repo, ReviewChannelAssociation
  • Identity & multiplayer: User, Principal (sealed UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Memory entities: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Shared value objects: AgentCapabilities, AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Shared ports: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Domain services: MemoryAccessPolicy, ActivityLogger, AgentLoopGuard
  • DomainEventBus + event types (workspace/agent, PR/review, messaging, ticketing, pipeline, orchestration, memory, calendar/meetings, identity & membership, observability)

Control Center is multi-user: humans and agents are co-equal actors unified by a Principal. A workspace member is a User bound to a Workspace at a WorkspaceRole (owner/admin/member/viewer/guest). Membership is the access boundary — not a pairing key — and per-repo grants keep workspace membership from out-privileging the forge. The first user is the workspace admin; others join by invite or OIDC.

Real-time collaboration is authoritative-server + per-field last-writer-wins (LWW), not a CRDT. Presence (who’s here, where they are, cursors, typing, an agent’s live status and running cost) is a separate ephemeral lane that is never persisted; durable state rides an optimistic-mutation + server-rebase + LWW backbone. Humans and agents share one roster. With a single human operator the presence lane idles and no roster chrome appears — multiplayer costs the solo user nothing.

Ports are abstract interfaces in the domain layer. Adapters are concrete implementations in infrastructure:

Port Adapter location
SandboxPort features/sandboxing/data/adapters/
CredentialBrokerPort features/sandboxing/data/
GitRepoInspectorPort features/repos/data/
EmbeddingPort core/infrastructure/
NotificationPort core/notifications/
AgentBackend features/dispatch/data/
TicketProviderPort features/ticketing/data/
PipelineEnginePort features/pipelines/data/

The composition root (di/providers.dart) binds ports to implementations via Riverpod providers.

Riverpod for all state:

  • Notifier<T> and AsyncNotifier<T> for mutable state
  • FutureProvider<T> for async data
  • Database-backed state returns AsyncValue<List<T>> from Drift .watch() streams
  • MCP tools receive dependencies as typed constructor parameters, never Ref

Drift (SQLite) with:

  • Tables defined in packages/cc_persistence/lib/database/tables/, DAOs in .../daos/ (schema v43, ~90 tables, v1-baseline squash with a continuous 1→43 MigrationStep chain)
  • Owned entirely by cc_persistence and opened only by cc_server; no client opens a DB — every client reads/writes over RPC
  • Domain entities are pure Dart, separate from Drift table classes; mapping happens in feature data layers
  • FTS5 for full-text search, sqlite_vector for embeddings; both server-only
  • PRAGMA foreign_keys=ON, WAL journal mode

go_router with:

  • ShellRoute wrapping the app shell
  • Auth guard redirects to /onboarding until GitHub auth + workspace exist
  • Route constants in router/routes.dart
  • Onboarding gate in features/auth/providers/

dio HTTP client with:

  • Specialized clients: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphqlClient, LinearApiClient, GoogleCalendarApiClient
  • Auth token injection via interceptors (including a per-account Google OAuth interceptor that refreshes tokens on 401)
  • All errors mapped through core/network/error_mapper.dart → typed AppException subclasses
  • API tokens in flutter_secure_storage (keychain/keystore/libsecret)
  • shared_preferences for non-sensitive preferences only
  • SecureCredentialsRepository abstracts storage from providers

Architecture constraints are validated by test/core/architecture_constraints_test.dart, which fails if dependency rules are violated.