Skip to content

Repository files navigation

Meridian — Engineering Documentation

Meridian is a local-first, privacy-focused productivity system built for macOS. It combines a spatial infinite canvas editor with structured daily tracking, designed for long-term reliability and "calm" computing.


1. System Architecture

Meridian follows a strict Main-Renderer separation pattern typical of secure Electron applications. The architecture prioritizes data integrity (ACID compliant SQLite) and UI responsiveness (React 18 concurrent features).

flowchart TD
    UI[Renderer - React]
    PRELOAD[Preload Bridge]
    MAIN[Main Process]
    DB[(SQLite DB)]
    FS[File System]

    UI -->|IPC Request| PRELOAD
    PRELOAD -->|Validated Call| MAIN
    MAIN --> DB
    MAIN --> FS
    MAIN -->|IPC Response| PRELOAD
    PRELOAD --> UI
Loading

Key Components

  • Renderer (/src): Handles all presentation logic. Uses Zustand for state management to avoid prop-drilling in the complex canvas component tree.
  • Main Process (/electron): The source of truth. Manages the database connection, file system access, and native window management.
  • Preload (/electron/preload.ts): A security sandbox that exposes specific API methods to the renderer, preventing direct Node.js access from the UI.

2. IPC Communication Flow

All data mutations occur via strictly typed IPC channels. We use the Request-Response pattern (ipcRenderer.invoke / ipcMain.handle) to ensure the UI waits for confirmation of persistence before updating its optimistic state (or handling errors).

Example: Save Note Sequence

sequenceDiagram
    participant UI
    participant Preload
    participant Main
    participant DB

    UI->>Preload: saveNote(content)
    Preload->>Main: ipc.invoke("save-note")
    Main->>Main: validateInput()
    Main->>DB: INSERT/UPDATE note
    DB-->>Main: success
    Main-->>Preload: result
    Preload-->>UI: success response
Loading

3. Rendering Pipeline & Spatial Editor

The core of Meridian is its spatial document editor (DocumentEditor), which supports mixed-media layout (text flow + floating shapes).

flowchart TD
    Edit[User Edits Block]
    Dirty[Mark Dirty State]
    Recalc[Recalculate Layout]
    Collision[Check Shape Collision]
    Reflow[Adjust Text Position]
    Render[Re-render Canvas]

    Edit --> Dirty --> Recalc --> Collision --> Reflow --> Render
Loading

Flow Cycle

  1. State Change: User adds a shape or resizes the text window.
  2. Intersection Calculation (useTextWrapping.ts):
    • The engine calculates the bounding box of every floating object.
    • It identifies which text "regions" (paragraphs/lines) intersect with these boxes.
    • It computes WrapRegion descriptors (left/right insets) for the text stream.
  3. Layout Reflow (TextDocument.tsx):
    • The text editor receives new wrapping regions.
    • It dynamically injects spacer elements or applies margins to text blocks to "flow" text around the shapes.
  4. Overflow Check:
    • If text pushes beyond the page bottom, the PageContainer detects overflow.
    • New pages are automatically created, and content is migrated to the next page.
  5. Paint: React commits the changes to the DOM.

Dirty State Logic

To minimize database writes, the editor uses a "dirty" state tracking mechanism:

  • Input: Sets isDirty = true.
  • Blur/Pause: Triggers save only if isDirty is true.
  • Save Complete: Sets isDirty = false.

4. Database Schema

Persistence is handled by better-sqlite3 in WAL (Write-Ahead Logging) mode for high concurrency and performance.

erDiagram
    DAYS ||--o{ DAILY_TASKS : contains
    DAYS ||--o{ HABIT_COMPLETIONS : tracks
    HABITS ||--o{ HABIT_COMPLETIONS : logs
    DATE_RANGES ||--o{ RANGE_TASKS : includes
    DAYS ||--o{ DAY_REFLECTIONS : stores
    NOTES {
        string id
        json content
        datetime updated_at
    }
Loading

5. Engineering Challenges & Design Decisions

Infinite Canvas Rendering Complexity

Problem: Managing an unbounded coordinate space where users can drag shapes anywhere without causing performance degradation or "lost" elements. Approach:

  • Coordinate System: We implemented a unified page-relative coordinate system. Instead of true infinite scrolling (which complicates printing/export), we use a "Virtual Page Stack".
  • Bounds Management: Shapes are children of the PageStack. Coordinates are calculated relative to the document, but rendered relative to current viewport.
  • Optimization: To prevent runaway DOM expansion, we use React virtualization techniques — only rendering pages currently near the viewport.
  • Reflow Stability: Naïve attempts to resize the canvas caused jitter. We moved to a fixed-width A4 model where only height expands, ensuring deterministic text flow at all times.

Text Wrapping vs. Shape Collision

The Challenge: The web platform (DOM) has no native API for "wrap text around this arbitrary absolute div". CSS float only works for elements in the same flow context. Failed Solution: Initially, we tried manual line-breaking by calculating character widths. This was slow, fragile, and broke with different fonts. Final Solution: We built a geometric collision engine (useTextWrapping.ts).

  • It projects 2D shape bounds onto the 1D text column.
  • It generates specific "exclusion rects" (WrapRegion).
  • The text renderer receives these rects and dynamically applies margin-left / margin-right to block-level elements that overlap. Trade-off: Text wrapping is block-level (paragraph/div), not per-line. This is a conscious decision to maintain performance (60fps) over typographic perfection.

IPC Validation Boundary

Security First: In Electron, the Renderer is untrusted (it runs user content). The Main process is privileged.

  • Isolation: The Renderer has zero direct access to the fs or sqlite modules. It cannot execute arbitrary SQL.
  • Bridge: The preload.ts context bridge only exposes specific methods (saveNote, getDays).
  • Validation: The Main process strictly validates all inputs.
    • Type Checking: Runtime checks ensure payloads match expected schemas.
    • Sanitization: JSON content (Notes) is parsed and structure-checked. HTML strings are sanitized via DOMPurify before storage/display to prevent XSS.

Persistence & Migration Strategy

Philosophy: Additive migrations only.

  • Reasoning: We never DROP COLUMN or modify existing data types. This ensures that:
    1. New versions of the app can safely open old databases.
    2. If a migration fails, the data remains intact (though potentially inaccessible to new features).
    3. Users can downgrade (within reason) without data corruption.
  • Implementation: On app startup, database.ts checks PRAGMA table_info. If a column (e.g., priority on tasks) is missing, it runs an ALTER TABLE statement. This is simple, robust, and zero-dependency.

6. What I Learned

Engineering Lessons

  1. Process Boundaries are Contracts: Treating the IPC layer as a strict API contract (like a REST API) prevented spaghetti code. Refactoring the backend didn't break the frontend because the "API" remained stable.
  2. State Separation: Trying to sync complex canvas state via prop-drilling in React was a mistake. Moving to Zustand stores for localized component updates was the single biggest performance win.
  3. Documentation as Infrastructure: Writing this documentation forced us to clarify the architecture. Several inconsistencies were found and fixed while writing the "System Architecture" section.
  4. Early Architecture Decisions: Choosing better-sqlite3 over Sequelize/TypeORM paid off. The raw SQL control provided predictable performance and simpler migrations without the "black box" overhead of an ORM.

7. Performance Considerations

Why better-sqlite3?

We chose a synchronous-interface SQLite driver because it runs in the Main process (Node.js). Being synchronous simplifies the code (no callback hell) and is actually faster for the scale of local data we process (< 100 queries/sec) because it avoids the overhead of context switching between JS and native C++ threads for every small query.

Manual Save vs. Autosave

We implement "Blur-to-Save" (save on focus loss) rather than continuous autosave during typing. This prevents database thrashing and race conditions while ensuring data is persisted effectively when the user pauses or switches contexts.


8. Testing Strategy

Current Checkpoints

  • Manual Regression Testing: Rigorous manual testing across "Day View", "Mind Canvas", and "Memory" stats before every release.
  • Migration Validation: Startup logic specifically tests for schema mismatches against previous versions.

Future Roadmap

  1. Unit Tests (Vitest): Planned coverage for useTextWrapping geometric logic and Zustand store reducers.
  2. Integration Tests: Planned IPC handler tests using an in-memory SQLite DB to verify validation barriers.
  3. Canvas Stress Tests: Edge-case testing for extreme zoom levels (10% - 300%) and deep page stacks (10+ pages).

9. CI/CD Roadmap

The goal is fully automated releases via GitHub Actions.

  1. Lint & Type Check (Current)
    • Runs eslint and tsc on every commit.
  2. Build Validation
    • npm run build:mac runs on PRs to ensure the build pipeline isn't broken.
  3. Auto-Release (Draft)
    • Trigger: Tag push v*.
    • Action: Build .dmg, draft GitHub Release, upload artifacts.
  4. Code Signing (Future)
    • Integrate Apple Developer ID certificate into CI secrets for notarized builds (required for auto-updates).

📦 Directory Structure

app/
├── electron/          # Main Process (Node.js context)
│   ├── main.ts        # Entry point, Window manager, IPC Handlers
│   ├── database.ts    # SQLite schema & migrations
│   └── preload.ts     # Context Bridge
│
├── src/               # Renderer Process (React context)
│   ├── components/
│   │   └── notes/     # Spatial Editor Engine
│   │       ├── document/  # Canvas & Text Logic
│   │       └── ...
│   ├── store/         # State Management
│   └── types.ts       # TypeScript Definitions
│
└── release/           # Build Artifacts

🛠 Build & Run

# Install dependencies
npm install

# Development (HMR)
npm run electron:dev

# Production Build (Mac Universal)
npm run build:mac

About

A thoughtfully designed Electron-based productivity app that combines daily task execution, spatial note-taking, and reflective memory views—built for calm focus, privacy, and long-term use.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages