Architecture
Overview
Section titled “Overview”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.
A thin client over a headless server
Section titled “A thin client over a headless server”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.
Dependency rule
Section titled “Dependency rule”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.
Feature structure
Section titled “Feature structure”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 providersTwo deliberate exceptions:
mcp,orchestration, andplan_studiouseapplication/instead ofpresentation/, because their proposal/plan-edit/tool logic is use-case code invoked by external clients or the server rather than UI screens.ticketingandnewsfeedaddmcp_tools/for tool metadata that binds their client presentation to the shared MCP registry.
Shared kernel
Section titled “Shared kernel”core/domain/ holds entities and repositories shared across 3+ features:
- Core entities:
Agent,AgentRunLog,Workspace,Repo,ReviewChannelAssociation - Identity & multiplayer:
User,Principal(sealedUserPrincipal|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)
Identity & multiplayer
Section titled “Identity & multiplayer”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 and adapters
Section titled “Ports and adapters”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.
State management
Section titled “State management”Riverpod for all state:
Notifier<T>andAsyncNotifier<T>for mutable stateFutureProvider<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
Database
Section titled “Database”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→43MigrationStepchain) - Owned entirely by
cc_persistenceand opened only bycc_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_vectorfor embeddings; both server-only PRAGMA foreign_keys=ON, WAL journal mode
Routing
Section titled “Routing”go_router with:
ShellRoutewrapping the app shell- Auth guard redirects to
/onboardinguntil GitHub auth + workspace exist - Route constants in
router/routes.dart - Onboarding gate in
features/auth/providers/
Networking
Section titled “Networking”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→ typedAppExceptionsubclasses
Security
Section titled “Security”- API tokens in
flutter_secure_storage(keychain/keystore/libsecret) shared_preferencesfor non-sensitive preferences onlySecureCredentialsRepositoryabstracts storage from providers
Architecture enforcement
Section titled “Architecture enforcement”Architecture constraints are validated by test/core/architecture_constraints_test.dart, which fails if dependency rules are violated.
Related concepts
Section titled “Related concepts”- Workspaces and isolation: how isolation is enforced
- Agent dispatch lifecycle: the dispatch flow
- Domain events: cross-feature communication
- Deployment and clients: the thin-client server model and the client matrix