Skip to content

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:

  1. Testability - Domain logic should be testable without spinning up Electron
  2. Portability - Core logic could potentially run in other contexts (CLI, web worker)
  3. Maintainability - Clear boundaries prevent accidental coupling
  4. 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 -> Storage

All 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