docs/references/main-process-architecture.md
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).
Exactly these, each with a single charter:
| Dir | Category | Why it earns a top-level home |
|---|---|---|
core | App runtime | Business-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). |
ipc | Cross-process boundary | Electron'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. |
data | Data layer | The 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. |
ai | Core domain | Cherry 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. |
features | Domain modules | Business domains, one directory each. A complex domain bundles its own related services / utils / etc. under features/<domain>/. |
services | Business services | Business feature services. A simple service is a single file; a larger one is organized into its own subdirectory. |
utils | Stateless helpers | Cross-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). |
i18n | Main-process localization | Main'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.
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
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:
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.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.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/:
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.services/<topic>/ subdirectory has exactly one index.ts as its public API (explicit named exports, no export *); its other files stay private behind it.utils/<topic>/ subdirectory likewise has one index.ts.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.)
The charters imply a direction; dependencies flow toward the business-agnostic foundation:
core/ and utils/ carry no business knowledge; nothing business sits below them.data/ is the storage layer above the foundation.ai/, 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.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.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.
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 isi18n/(§1), added so the main process owns a locale catalog symmetric withsrc/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 essence | ai/ |
| business data / storage | data/ |
| an IPC route | ipc/ (IpcApi) |
| business-agnostic, non-removable app-runtime infra | core/ |
| a business service | services/ — or features/<domain>/ if it is a large, multi-file domain |
| pure, domain-agnostic logic | utils/ |
core/ — core/ must stay app-runtime-only.features/<domain>/ importing a sibling feature (cross-domain coupling).ai/ importing features/ (the core domain depending up on a feature).data/ subsystems, or imperative commands through ad-hoc channels instead of ipc/ (IpcApi).Per-subsystem depth lives in dedicated docs; this page owns only the directory layout. Do not duplicate subsystem detail here.
| Subsystem | Location | Reference |
|---|---|---|
| Service lifecycle (IoC, phased bootstrap) | core/lifecycle/, core/application/ | Lifecycle Reference |
| Startup phases (preboot / bootstrap / running) | core/preboot/, core/application/ | core/README |
| Window manager | core/window/ | Window Manager Reference |
| Scheduler & jobs | core/scheduler/, core/job/ | Job & Scheduler Reference |
| Path registry | core/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 subsystem | ai/ | AI Reference |
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.
| Area | Current | Target |
|---|---|---|
utils/index.ts | a bucket-root index.ts holds loose helpers (debounce, makeSureDirExists, toAsarUnpackedPath, …) — the junk-drawer root barrel §2.1 forbids | split into named topic files (utils/<topic>.ts); @shared has already done exactly this — see Shared Layer Architecture §6 |
legacy ipc.ts | v1 IPC registration at the process root, coexisting with IpcApi | domains 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).