Status: team standard
Reference implementation: src/features/recent-projects
This document defines the default architecture for medium and large features in this repository.
- keep business rules isolated from Electron-specific runtime details
- make features easier to scale, test, and review
- keep renderer code closer to browser and Tauri portability
- enforce architecture with tooling, not only with code review comments
This standard uses a feature-first, process-aware Hexagonal Architecture with DDD domain modeling. It is not a one-to-one directory translation of the classic DDD layers.
The structure combines three explicit concerns:
core/domainandcore/applicationexpress business and use-case ownershipmain,preload, andrendererexpress Electron process boundaries- ports and adapters express the dependency direction between the application core and external transports, providers, storage, and runtime services
These concerns are complementary, but their names must not be used interchangeably. In particular, a renderer projection that only reshapes data for UI rendering is a view model, not a Hexagonal Architecture adapter.
Dependency direction is inward:
adapters -> application -> domain
Outer process layers may depend on contracts and core; core must not
depend on Electron process layers or concrete adapters.
src/features/<feature-name>/
index.ts
contracts/
index.ts
core/
domain/
application/
main/
index.ts
composition/
application/
adapters/
input/
output/
infrastructure/
preload/
index.ts
renderer/
index.ts
Use this template by default when a feature:
- spans more than one process boundary
- introduces its own use case or business policy
- needs its own transport bridge or integration surface
- is expected to grow with new providers, sources, or presentation flows
index.ts and main/application/ are optional. Add them only when they have a
clear public or runtime-orchestration role.
Optional root public barrel for the feature.
Use it for:
- stable type re-exports from
contracts/ - small pure facades that are intentionally shared across layers
- feature factories when the root barrel is intentionally main-owned and imported only from main-process code
Not allowed:
- accidental wildcard exports from implementation folders
- mixing browser-safe exports with main-only exports without making process ownership obvious
- replacing the layer entrypoints when callers need a process-specific surface
Cross-process public API for the feature.
Allowed content:
- DTOs
- API fragment types
- IPC or route constants
Not allowed:
- store access
- Electron APIs
- business orchestration
Pure domain models, business rules, and invariants. This is the DDD modeling layer inside the feature.
Examples:
- merge policies
- provider-agnostic models
- selection rules
- dedupe logic
Not allowed:
- infrastructure access
- framework access
- side effects
Use cases and the ports they own. A port describes an application conversation
or required capability; concrete runtime implementations stay outside core/.
Examples:
- orchestration flow
- output ports
- cache ports
- source ports
- response models
Not allowed:
- Electron, Fastify, React, Zustand, child processes
Feature composition root in the main process.
Responsibilities:
- instantiate infrastructure
- wire adapters
- wire use cases
- expose a small facade to app shell entrypoints
Driving adapters for the main process.
Examples:
- IPC handlers
- HTTP route registration
Responsibilities:
- translate transport input into use case calls
- keep transport concerns out of use cases
- depend inward on the application surface, never the reverse
Optional main-process application services.
Use this only when code is too runtime-aware for core/application, but is not a
transport adapter or low-level infrastructure helper.
Examples:
- main-only readers that orchestrate runtime services
- process-aware tracking or coordination helpers
Not allowed:
- IPC or HTTP handler registration
- renderer or preload dependencies
- pure domain policy that belongs in
core/domain
Driven adapters that implement application ports.
Examples:
- presenters
- source adapters
Responsibilities:
- translate between external data and core models
- stay thin around infrastructure helpers
- implement application-owned ports when the implementation represents an external conversation such as a provider source, presenter, or repository
Concrete technical implementation details.
Examples:
- file system adapters
- JSON-RPC transport clients
- binary discovery
- cache implementation
- git identity helpers
Responsibilities:
- know about runtime, process, OS, or protocol details
- provide low-level technical mechanisms used by adapters and composition
- avoid owning domain policy or transport registration
Use main/adapters/output when a component is named by an application
conversation and translates between an application port and an external
system. Use main/infrastructure when a component is named by its technical
mechanism and exposes a lower-level runtime primitive. A small technical port
implementation may be wired directly from infrastructure when no translation
layer would add meaning.
Thin transport bridge between renderer and main.
Responsibilities:
- expose a feature API fragment
- depend on
contracts/
Not allowed:
- main composition code
- renderer logic
Feature presentation and interaction.
Recommended structure:
renderer/
index.ts
hooks/
ui/
utils/
view-models/
adapters/ # optional, only for actual external boundaries
Responsibilities:
ui/rendershooks/orchestrate interaction and transport usageview-models/transform DTOs or application data into presentation modelsadapters/integrate genuine browser, storage, API, framework, external package, or legacy UI boundaries; do not use this name for ordinary DTO-to-view-model mappingutils/contain small pure renderer helpers
Build interactive UI controls from reusable shared components backed by Radix UI
headless primitives. Prefer existing components under src/renderer/components/ui
for controls such as selects, dialogs, popovers, tabs, menus, tooltips, switches,
and checkboxes. Avoid hand-rolled or native controls when a shared Radix-based
primitive exists; add or extend the shared primitive instead of styling a one-off
control inside a feature.
For user-facing hover or focus help, use the shared Radix Tooltip primitives from
src/renderer/components/ui/tooltip.tsx. Do not use the native HTML title
attribute as a tooltip: it cannot follow the app theme or reliably avoid dialog,
popover, and viewport boundaries. Keep accessible names and descriptions in
aria-label, aria-description, or an explicit aria-describedby relationship.
Outside the feature, import only:
@features/<feature>when the feature owns a deliberate root public barrel@features/<feature>/contracts@features/<feature>/main@features/<feature>/preload@features/<feature>/renderer
Do not deep-import feature internals from app shell or from other features.
Layer entrypoints should be explicit index.ts files that export only supported
surface area. Focused tests may import internals when they are testing that unit
directly, but production integration code should not.
core/domain must not import:
@main/*@renderer/*@preload/*- adapters
- infrastructure
- Electron APIs
- Fastify
- child process modules
core/application must not import:
@features/<feature>/main/**@features/<feature>/renderer/**@main/*@renderer/*@preload/*- Electron APIs
- Fastify
- child process modules
renderer/ui must not import:
@renderer/api@renderer/store@main/*- Electron APIs
Push transport and store access into feature hooks or genuine boundary
adapters. Keep presentation-only projection in renderer/view-models/.
The default transport direction should be:
renderer -> feature contracts -> app api abstraction -> preload/http adapter
This keeps renderer code closer to:
- browser mode through HTTP adapters
- a future Tauri bridge
- alternative shells with minimal feature rewrites
To keep that path clean:
- never call
window.electronAPIdirectly inside feature UI or hooks - go through shared renderer API adapters
- keep Electron-specific concerns in
main/andpreload/ - keep business rules in
core/
For any medium or large feature that is not inherently desktop-only, design the feature so the same user-visible workflow can function through the HTTP/server adapter path.
This is critical because the HTTP path is the most portable integration surface:
- browser mode can use it directly
- Electron can keep using IPC without changing feature semantics
- future shells can add their own transport without rewriting the feature
- tests can exercise the same use cases without Electron-specific wiring
The rule is:
- the feature contract belongs in
contracts/ - the use case belongs in
core/applicationor a main-owned application service - IPC handlers and HTTP routes are only transport adapters
- both adapters call the same feature facade or use case
- renderer code goes through the app API abstraction and must not know whether the active transport is IPC or HTTP
Do not implement business behavior only inside an IPC handler. If desktop needs native capabilities, put those capabilities behind ports or main-process services, then expose the workflow through the same feature facade. Only skip HTTP/server support when the feature is explicitly desktop-only, for example a native window management operation with no meaningful browser equivalent.
When adding or changing feature APIs, keep transport parity explicit:
- add or update IPC coverage for the Electron desktop path
- add or update HTTP route/client coverage for browser/server mode
- document any unsupported browser-mode behavior as a deliberate exception
- add tests or typed contract checks when drift between transports would be risky
Use the full template when a feature has:
- its own business rules
- its own merge or filtering policy
- transport wiring
- more than one adapter
- a roadmap beyond a one-off screen tweak
A smaller feature may skip core/ and preload/ when it is:
- purely presentational
- only reshaping already-owned data
- not adding a new use case
- not adding a new transport boundary
If the feature still owns meaningful pure semantics or projection rules, keep
core/ and skip only the process layers you do not need.
Example:
src/features/agent-graphkeepscore/domainandrenderer, but does not add fakemain/orpreload/folders because the transport boundary lives elsewhere.
When migrating a legacy service, keep its stable public facade while moving one observable vertical slice at a time into the canonical feature layers.
- prefer explicit composition over inheritance
- use inheritance only for genuine substitutable subtypes, never to aggregate dependencies or share mutable service state
- do not pass the legacy service as a generic host to new use cases
- define small ports owned by the consuming application use case
- give mutable state one explicit repository, registry, coordinator, or gate
- delegate from the legacy facade until callers can use the new public entrypoint
- remove migrated compatibility wiring in the same slice when safe
- do not create empty feature scaffolding before a real use case needs it
For the production-critical Team Provisioning migration, follow the concrete
target and strangler protocol in
team-provisioning-target-architecture.md.
Use these local examples before inventing a new variant:
src/features/recent-projects- full cross-process reference withcontracts,core,main,preload, andrenderer.src/features/member-work-sync- full cross-process feature with a root public barrel and broader main-process infrastructure.src/features/member-log-stream- full cross-process feature that usesmain/application/for main-only runtime orchestration.src/features/agent-graph- thin renderer integration withcore/domainandrenderer, no fake process layers.src/features/codex-model-catalogandsrc/features/team-runtime-lanes- process-limited features that omit renderer or preload layers when they do not own that boundary.
A feature is reference-quality when:
- structure matches the full or thin template chosen for the feature
- core is side-effect free
- app shell imports only public entrypoints
- renderer UI is dumb and presentational
- browser/server mode is first-class for non-desktop-only workflows
- at least the main domain and application rules are tested when those layers exist
- architecture is enforced by lint rules
- feature has a concise standard or plan doc if it introduces a new pattern
For medium and large features, cover at least:
- domain policy tests
- application use case tests
- critical renderer interaction utilities
- one adapter-level mapping test
src/features/recent-projects is the first slice that follows this standard end-to-end.
Use it as the example for:
- contracts ownership
- core/application separation
- composition-root wiring
- renderer dumb UI + hook orchestration
- browser-friendly transport direction
- feature-level lint guard rails
src/features/agent-graph is the thin-slice example for a renderer integration
feature built on top of a reusable package.
Use it as the example for:
- keeping pure graph semantics in
core/domain - exposing a renderer-only public entrypoint
- integrating
packages/agent-graphwithout inventing fake process layers - migrating legacy
src/renderer/features/*code into the canonical feature root