ADR-001: Runtime Contract
Status
Accepted
Context
Readied is an Electron desktop app with complex domain logic (note management, markdown parsing, metadata extraction). We need to decide how to structure the codebase to ensure:
- Testability - Domain logic should be testable without spinning up Electron
- Portability - Core logic could potentially run in other contexts (CLI, web worker)
- Maintainability - Clear boundaries prevent accidental coupling
- Security - Renderer process should have limited access to system resources
Decision
We adopt a strict separation of concerns with these boundaries:
Core Package (@readied/core)
- Contains all domain logic
- Zero dependencies on Electron, React, or Node.js-specific APIs
- Testable in pure Node.js with Vitest
- Exposes Commands (mutations) and Queries (reads)
- Uses ports/adapters pattern for I/O
typescript
// Core exposes operations, not implementation details
export { createNoteOperation } from './operations/createNote';
export { getNoteOperation } from './operations/getNote';
// Repository is an interface, not an implementation
export interface NoteRepository {
get(id: NoteId): Promise<Note | null>;
save(note: Note): Promise<void>;
}Storage Packages
@readied/storage-core- Interfaces and utilities (no native deps)@readied/storage-sqlite- SQLite implementation (native deps isolated)
Desktop App
- Main process: Database initialization, IPC handlers
- Preload: Minimal typed API bridge
- Renderer: React UI, calls preload API
Communication Contract
Renderer -> Preload -> IPC -> Main -> Core -> StorageAll IPC channels are typed. No raw SQL exposed to renderer.
Consequences
Positive
- Testable: Core has 52 unit tests running in <500ms
- Portable: Core could run in Deno, Bun, or web workers
- Secure: Renderer can't access filesystem directly
- Maintainable: Changes to UI don't affect domain logic
Negative
- Boilerplate: More layers means more code
- Indirection: Data flows through multiple boundaries
- Learning curve: Contributors must understand the architecture
Risks
- Over-engineering for a solo project
- Mitigation: Keep packages minimal, split only when there's pain
Alternatives Considered
1. Single Package
Put everything in one package with folder-based separation.
Rejected because:
- Easy to accidentally couple layers
- Can't enforce boundaries at build time
- Testing requires mocking Electron
2. Domain-Driven Modules
Split by domain (notes, tags, links) instead of by layer.
Rejected because:
- Cross-cutting concerns (storage, IPC) become messy
- Harder to swap implementations
- Less clear security boundaries
3. Full Monolith
No separation, everything in desktop app.
Rejected because:
- Untestable without Electron
- Impossible to reuse logic
- Security risks from coupled code