Back to Cherry Studio

Main Process Architecture (`src/main`)

docs/references/main-process-architecture.md

2.0.014.4 KB
Original Source

Main Process Architecture (src/main)

This is the canonical reference for how src/main/ is organized: what each top-level directory is for, the rules that keep them from sprawling, and how they depend on each other. It is the main-process peer of Renderer Architecture and Shared Layer Architecture; the cross-process picture (process model, monorepo tree) lives in Architecture Overview.

The top level is a closed, locked set of principled categories, not an open list of modules. Each top-level directory holds one kind of thing and earns its place for a distinct reason. The set is locked — a new capability is always routed into an existing category by its nature, never given a new top-level directory (§4).

1. The Closed Top-Level Set

Exactly these, each with a single charter:

DirCategoryWhy it earns a top-level home
coreApp runtimeBusiness-agnostic infrastructure concerned only with running the app. The test: lift core/ onto a different Electron app, add other business code, and you have a different application. Business-agnostic is necessary, not sufficient: core holds only the non-removable substrate the app cannot run without — removable capabilities route to services/ even when they must execute early in startup (§4). One kind of thing — the app substrate: lifecycle / DI container, path registry, logger, window manager, scheduler & jobs, preboot, diagnostics, security primitives (IPC source trust).
ipcCross-process boundaryElectron's defining inter-process mechanism — special and important enough to stand alone. Unified as IpcApi (schema + router + handler): the single typed boundary between main and renderer.
dataData layerThe general business-data store — a first-class data layer, hence independent. Holds DbService / CacheService / PreferenceService / DataApiService / BootConfig, DB schemas, and the v1→v2 migrators (which by design read domain data — throwaway migration code). Detailed in Data System Reference.
aiCore domainCherry Studio is an AI client, so AI earns its own top-level home: everything tied to the AI essence lives here (providers, middleware, MCP, agents, stream manager). Mirrors @shared/ai.
featuresDomain modulesBusiness domains, one directory each. A complex domain bundles its own related services / utils / etc. under features/<domain>/.
servicesBusiness servicesBusiness feature services. A simple service is a single file; a larger one is organized into its own subdirectory.
utilsStateless helpersCross-domain stateless, domain-agnostic functions with no single owner. "Stateless" is the bar, not "pure": a helper may reach infra through the ambient @application / @logger (§3); it just owns no state and performs no outward side effects (§2).
i18nMain-process localizationMain's own locale catalog (locales/ human + translate/ machine) and its t() / getI18n() resolver. A deliberate, governed expansion of the closed set (§4), mirroring src/renderer/i18n/ so each process owns an independent catalog; the utils/i18n/ alternative was rejected for that cross-process symmetry.

Entry files: main.ts (process entry — runs preboot, then application.bootstrap(); a named file, since index is reserved for barrels per Naming §6.4) and ipc.ts (legacy IPC registration, being retired into ipc/).

Naming follows Naming Conventions §4.9: core / data / ai / ipc / i18n are singular namespaces; features / services / utils are plural buckets.

text
src/main/
├── main.ts      # process entry: preboot → application.bootstrap()
├── ipc.ts       # legacy IPC registration (being retired into ipc/)
├── core/        # business-agnostic app runtime (lifecycle/DI, paths, logger, window, scheduler/job, preboot, security)
├── ipc/         # IpcApi — the typed main↔renderer boundary
├── data/        # the data layer (DB/Cache/Preference/DataApi/BootConfig, schemas, migration)
├── ai/          # the AI subsystem — the product's core domain
├── features/    # business domains, one dir each (each bundles its own services/utils)
├── services/    # business feature services (single file, or a subdirectory)
└── utils/       # cross-domain stateless helpers

2. features vs services (Placement)

services/ and features/ are the same kind of thing — business logic — at two sizes. The split follows the cross-process rule in Naming Conventions §4.10:

  • Promotion, not default — and in steps. A small, self-contained service starts as a single file at the bucket root — services/<Topic>Service.ts (a stateful Service/Manager class is PascalCase matching its class name, Naming §5.2; a generic helper is utils/<topic>.ts — a topic-specific one stays inline until the subdirectory step below). When one file can no longer hold it, grow it in place into a camelCase topic subdirectory first — services/<topic>/ holding <Topic>Service.ts plus its helpers — not straight into a feature. Mind the shape: the directory is the topic name and carries no Service suffix (Naming §4.5); only the class file keeps the suffix (e.g. services/webSearch/WebSearchService.ts). It earns a features/<domain>/ home only once it grows into a large, multi-file domain bundling its own services, utils, and helpers (knowledge, apiGateway, fileProcessing). Do not pre-create a subdirectory or a feature for an anticipated module.
  • ai/ is not an ordinary feature. It is the product's core domain and has its own top-level home (§1); it is foundational, not one domain among many.
  • Route by role (Naming Conventions §5.2): a stateful class owning long-lived resources or persistent side effects → a lifecycle Service (Lifecycle Reference); a stateless module → utils/ by default, promoted to services/ only by outward side effects or a forced upward dependency (§5.2 routing table); a large domain → features/<domain>/. Reads never promote — a helper touching infra for queries is still a helper.

2.1 Subdirectories and Barrels

A single .ts file is the default; promote a topic to a subdirectory only when it actually owns multiple files. Barrels follow Naming §6.4 (the cross-process authority), applied here to services/ and utils/:

  • The bucket roots services/ and utils/ have no index.ts. A bucket is a category, not a module — import the specific file or topic, never the whole bucket.
  • A services/<topic>/ subdirectory has exactly one index.ts as its public API (explicit named exports, no export *); its other files stay private behind it.
  • A complex utils/<topic>/ subdirectory likewise has one index.ts.
  • Why: each topic is then imported through a single public entry — exactly like a one-file module — so its internal files stay private and consumers never deep-import. For utils/, where file and directory share the topic name, the specifier (@main/utils/<topic>) is even unchanged when a file grows into a folder.

(A features/<domain>/ is the same single-entry idea one tier up: consumers import the domain through its one public entry, not its internal files.)

3. Dependency Direction

The charters imply a direction; dependencies flow toward the business-agnostic foundation:

  • Foundationcore/ and utils/ carry no business knowledge; nothing business sits below them.
  • Data layerdata/ is the storage layer above the foundation.
  • Businessai/, features/, and services/ are the business tier; they depend down on data/, core/, and utils/.
  • ai/ is foundational within the business tier: features/ and services/ may depend on it; it must not import a feature.
  • Feature domains are mutually isolated: a features/<domain>/ must not import a sibling feature — share through services/, ai/, data/, or @shared.
  • ipc/ is the boundary adapter: a handler stays thin (boundary policy + IpcError mapping + delegation) and reaches business code two ways — a lifecycle service (registered in serviceRegistry.ts) is resolved via application.get('XxxService'), never imported directly; a non-lifecycle module (stateless topic barrel or direct-import singleton) is imported through its curated entry, and fabricating a lifecycle service just to obtain a DI handle is an anti-pattern. See Handler: Pure Function vs Service Delegate.
  • No renderer imports: src/main and src/preload must not import renderer code. Cross-process types live in @shared, main-only types stay in src/main — see Shared Layer Architecture for placement. Enforced by an ESLint no-restricted-imports rule banning @renderer in src/main + src/preload; §7 tracks the one remaining exception.

Two dependencies cut across every directory and are not layering edges — they are ambient infrastructure access: @logger (logging) and @application (the DI container / service locator). A raw import scan shows almost everything "depending on core" only because of these two; the rules above concern direct module imports between domains.

There is no automated enforcement of the internal direction edges above yet (unlike the import/no-restricted-paths zones proposed for the renderer in Renderer Architecture §5); direction is held by convention and review. The external main↔renderer boundary (no renderer imports) is enforced — see the rule above.

4. Closed Top-Level Governance

The top-level set is closed and locked — treat adding a new directory under src/main/ as off the table. This is Naming Conventions §4.8 (top-level closed by default) at its strict end: §4.8 admits a new top-level directory only on proven necessity (no existing category can host the files) and completeness, and main's categories already span the space — so a new capability is routed into an existing category, never given its own directory. The one deliberate expansion is i18n/ (§1), added so the main process owns a locale catalog symmetric with src/renderer/i18n/; it is a governed exception with a recorded rationale, not a loosening of the rule. The renderer (§6) and @shared (§2) top levels are held to the same governance.

A new capability never earns a new top-level directory; route it by nature:

The capability is…Home
tied to the AI essenceai/
business data / storagedata/
an IPC routeipc/ (IpcApi)
business-agnostic, non-removable app-runtime infracore/
a business serviceservices/ — or features/<domain>/ if it is a large, multi-file domain
pure, domain-agnostic logicutils/

5. Anti-Patterns

  • Business code (anything specific to what Cherry Studio does) placed in core/core/ must stay app-runtime-only.
  • A features/<domain>/ importing a sibling feature (cross-domain coupling).
  • ai/ importing features/ (the core domain depending up on a feature).
  • Opening a new top-level directory for a single capability (§4).
  • Scattering business data through ad-hoc storage instead of the data/ subsystems, or imperative commands through ad-hoc channels instead of ipc/ (IpcApi).

6. Subsystem References

Per-subsystem depth lives in dedicated docs; this page owns only the directory layout. Do not duplicate subsystem detail here.

SubsystemLocationReference
Service lifecycle (IoC, phased bootstrap)core/lifecycle/, core/application/Lifecycle Reference
Startup phases (preboot / bootstrap / running)core/preboot/, core/application/core/README
Window managercore/window/Window Manager Reference
Scheduler & jobscore/scheduler/, core/job/Job & Scheduler Reference
Path registrycore/paths/paths/README
IPC source-trust gate (validateSender)core/security/IpcApi Overview §Security
Data systems (DB/Cache/Preference/DataApi/BootConfig)data/Data System Reference
IPC (IpcApi)ipc/IPC Reference
AI subsystemai/AI Reference

7. Current Deviations (target vs current)

This page describes the target. Where current code does not yet match it, the gap is tracked below — this pass changes no code. Only structural deviations are listed (the closed top-level set §4, bucket barrels §2.1, placement §2); a per-file naming-suffix audit (§5.2) is out of scope here.

AreaCurrentTarget
utils/index.tsa bucket-root index.ts holds loose helpers (debounce, makeSureDirExists, toAsarUnpackedPath, …) — the junk-drawer root barrel §2.1 forbidssplit into named topic files (utils/<topic>.ts); @shared has already done exactly this — see Shared Layer Architecture §6
legacy ipc.tsv1 IPC registration at the process root, coexisting with IpcApidomains migrate into ipc/ (IpcApi) incrementally until ipc.ts is retired (§1)

By-design edges are not deviations and are deliberately omitted: the data/migration/v2/ migrators reading domain data (§1) and @logger / @application ambient access from any tier (§3).