Architecture
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
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-one members — fifteen packages plus six apps — with the desktop+web app (control_center) as the workspace root. The six 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), the Widgetbook gallery (cc_gallery) and the public demo host (cc_demo_server). 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 minted on a fresh server owns the install; others join by invite or OIDC. Creating a workspace stamps that user as owner.
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:
| Port | Adapter location |
|---|---|
SandboxPort |
cc_infra/lib/src/sandboxing/ (native_sandbox_adapter.dart) |
CredentialBrokerPort |
cc_infra/lib/src/sandboxing/ (env_credential_broker.dart, github_fine_grained_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/ (LinearTicketAdapter). Jira, ClickUp and GitHub Issues are ticket-sync adapters in cc_infra/lib/src/tickets/sync/; local tickets live in the workspace database with no TicketProviderPort. |
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 client state:
Notifier<T>andAsyncNotifier<T>for mutable stateFutureProvider<T>for async data- Lists and live rows arrive over RPC subscriptions as
AsyncValue<List<T>>— the server uses Drift.watch()internally; no client opens Drift - 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, managed action policies (an install-wide clamp that can only tighten workspace guardrails), 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 - Four pre-context routes are unprefixed:
/splash,/onboarding,/signed-outand/workspaces(the picker) ShellRoutewraps the app shell; those four surfaces render full-screen outside it- Auth guard: incomplete setup goes to
/onboarding; a completed setup whose forge credential is gone goes to/signed-out. Complete means at least one forge is connected for the signed-in user and at least one workspace exists — not GitHub specifically, and theghCLI is not an authentication method - 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,GitLabApiClient,BitbucketApiClient,LinearGraphQlClientandGoogleCalendarApiClient - Auth token injection via interceptors, including a per-account Google OAuth interceptor in
cc_server_corethat 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 device credential and the pairing key for a remote server.
- Secrets in
flutter_secure_storage(macOS keychain, Windows credential manager, Linux libsecret) — the client’s device credential and remote pairing keys, not provider tokens shared_preferencesfor non-sensitive preferences only (theme, font, layout)- Leftover provider secrets from older installs are handed to the server once by
features/auth/providers/credential_migration.dart, then deleted from the client keychain
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