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 small global.db plus one SQLite file per workspace, opened lazily) 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.
The RPC layer itself is stateless: a session is authenticated, but it is not bound to a workspace. Every workspace-scoped call carries its own workspace_id in its arguments and the dispatcher refuses such a call without one. That is what lets two clients on one server hold different active workspaces without a server-held “current workspace” to disagree about — and it means isolation is enforced per call, not per connection.
This is a single resolved-lockfile Dart workspace of twenty members — fifteen packages plus five apps — with the desktop+web app (control_center) as the workspace root. The five apps are the headless server binary (cc_server), the headless fleet executor (cc_worker), the phone PWA (cc_remote), the WebSocket relay broker (cc_signaling_server) and the Widgetbook gallery (cc_gallery). The fifteen packages are cc_ui (design system), cc_domain (pure-Dart shared kernel: entities, value objects, ports, events and every feature’s domain layer), cc_harness (the built-in agent kernel: a pure-Dart agent loop — messages, provider port, tools, compaction, steering, hooks, subagents — with no dart:io and no other cc_* dependency), cc_harness_runtime (the built-in harness runtime adapter: VM-only streaming providers, OAuth/PKCE credential brokering, credential stores and the generic tool set — runs agents with no external CLI installed), 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_mcp_client (client for external MCP servers, bridging their tools into the local registry), 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”The full feature shape is:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersVery few features under lib/features/ actually carry all four and the gaps are informative rather than accidental. The shared kernel absorbed every feature’s domain/ into cc_domain and the thin client holds no repository implementations, so most features under lib/ are presentation/ plus providers/ and nothing else. A data/ or domain/ directory that survives in the client marks something genuinely client-side.
Two named exceptions are worth knowing:
mcpis providers-only. Its settings and status UI lives undersettings/and the tool surface itself is a package (cc_mcp), not a feature folder.orchestrationandplan_studiocarry onlypresentation/andproviders/.
The guardrails feature puts its domain layer in cc_domain/features/guardrails/ rather than the root app, because the server enforces it: a rule maps an ActionClass (or a shell commandPrefix) to a decision — allow, prompt, or deny — at a scope, rules are stored per workspace and every governed agent action resolves through the policy chain before it runs. Resolution walks space > agent > workspace > mode preset > built-in default and the first scope with a matching rule decides; most-restrictive is only a tie-break within one scope and for combining the several classes one action declares. The operator-facing surface is Settings → Workspace → Agent permissions, at /workspaces/<id>/settings/workspace/permissions. See Guardrails for the model.
Shared kernel
Section titled “Shared kernel”packages/cc_domain/lib/core/domain/ holds entities and repositories shared across 3+ features. It is pure Dart with zero infrastructure dependencies, which is what lets both the Flutter client and the Flutter-free server binary import the same definitions:
- Core entities:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - 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,MentionResolver,RunLivenessClassifier 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. Because the headless server owns the data and must build Flutter-free, nearly every adapter is VM-only and lives in packages/cc_infra/lib/src/, organized by concern — the root app’s feature folders no longer carry data/adapters/:
| Port | Adapter location |
|---|---|
SandboxPort |
cc_infra/lib/src/sandboxing/ (native_sandbox_adapter.dart) |
CredentialBrokerPort |
cc_infra/lib/src/sandboxing/ (env_credential_broker.dart, task_scoped_credential_broker.dart) |
GitRepoInspectorPort |
cc_infra/lib/src/git/ (git_repo_inspector.dart) |
EmbeddingPort |
cc_infra/lib/src/embedding/ (embedding_service.dart) |
NotificationPort |
lib/core/notifications/ (client-side: renders server events locally) |
AgentBackend |
cc_infra/lib/src/dispatch/backends/ (acp_backend.dart, harness_backend.dart, cli_backends.dart) |
TicketProviderPort |
cc_infra/lib/src/tickets/ (Linear, Jira, ClickUp, local) |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (a pure-Dart domain service — it needs no infrastructure) |
The app composition root (di/providers.dart) binds client-side ports to implementations via Riverpod providers; the server composes its adapters in cc_server_core.
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), split into two databases, both in cc_persistence:
GlobalDatabase(<dataDir>/global.db) — a short list of genuinely server-wide tables: the workspace registry, users/preferences/paired devices, SSO connections, install-wide server settings, the per-user newsfeed, the fleet queue (workers/jobs/placement log), the pre-authworkspace_routesindex and install metadata. Boot opens only this file, so startup cost stays flat no matter how much history the workspaces accumulate. Adding a table here is an isolation decision a ratchet test forces you to argue for.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — one directory per workspace, holding one SQLite file plus everything else that belongs only to that workspace, so a conversation’s worktrees and agent files sit beside the database and are deleted with it. Nearly everything lives here: agents, spaces, tickets, memory, pipelines, meetings, the code graph, reviews, repos. Files open lazily on first touch throughWorkspaceDatabaseManager.of(workspaceId)and pay their ownquick_check, FTS/trigger install andvector_initthen.
Both halves started from a squashed v1 baseline — onCreate builds the current schema — with migration steps appended only when a deployed database has to be carried forward.
Because one workspace is one file, exporting a workspace is a single VACUUM INTO statement (workspace.export), not a table-by-table dump; workspace.import adopts such a file and uses the embedded workspace_meta to tell its own file re-adopted from one that came from another install.
Both databases are owned by cc_persistence and opened only by cc_server; no client opens a DB — every client reads/writes over RPC. Tables are defined in packages/cc_persistence/lib/database/tables/, DAOs in .../daos/; domain entities are pure Dart, separate from Drift table classes, with mapping in the data layers. FTS5 handles full-text search and sqlite_vector embeddings, both server-only; both databases run PRAGMA foreign_keys=ON in WAL journal mode.
Routing
Section titled “Routing”go_router, with one fact that governs the rest: every in-app destination is workspace-prefixed as /workspaces/:workspaceId/…. The workspace id in the URL is the single source of truth for the active workspace — activeWorkspaceIdProvider is driven from the route, not the other way round — which is why reading workspace-scoped data outside a prefixed route has nowhere to get its id from.
- Route builder functions in
router/routes.dart, each taking the workspace id as its first argument, not string constants - Only three pre-context routes are unprefixed:
/splash,/onboardingand/workspaces(the picker) ShellRoutewraps the app shell; splash and onboarding render full-screen outside it- Auth guard redirects to
/onboardinguntil GitHub auth and at least one workspace exist - Onboarding gate in
features/auth/providers/
Networking (server-side)
Section titled “Networking (server-side)”All external HTTP lives in the server. Clients never dial GitHub, Linear or Google directly — they call server RPC ops and even remote media is fetched through the server’s /proxy/media endpoint rather than from the upstream origin.
- dio clients in
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphqlClient,LinearApiClientand the Google Calendar REST client - Auth token injection via interceptors, including a per-account Google OAuth interceptor that refreshes tokens on 401
- All errors mapped through
packages/cc_infra/lib/src/network/error_mapper.dart→ typedAppExceptionsubclasses defined incc_domain
Security
Section titled “Security”The server holds the credentials that matter: GitHub and ticketing tokens for agent launches, LLM provider keys and OAuth tokens all live under the server data dir or its environment. What the client keychain still holds is its own connection material and the credentials it was configured with directly.
- Secrets in
flutter_secure_storage(macOS keychain, Windows credential manager, Linux libsecret), including the pre-shared key for a paired remote server shared_preferencesfor non-sensitive preferences only (theme, font, layout)SecureCredentialsRepositoryabstracts the store 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