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