Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture and Data Model

Use a Kotlin Multiplatform modular core, with native Android and iOS presentation layers. Keep UI-specific editor and navigation code outside shared code; share the domain model, use cases, persistence abstractions, future-sync contracts, and test fixtures. Release 1 does not contain a running sync engine or destination adapter.

Android native UI (Kotlin)       iOS native UI (Swift/SwiftUI or UIKit)
          |                                 |
          +---------- KMP presentation-facing use cases ----------+
                                      |
      domain model | revision service | search | future-sync contracts
                                      |
                repository and capability interfaces
                   |                |                  |
          local database   (future adapters)     platform services

Suggested modules

ModuleResponsibility
shared:domainEntities, validation, use cases, conflict and revision rules
shared:dataRepository implementations, serialization, database mappings, migrations
shared:sync-contractsPlaceholder-only capability and adapter contracts; no coordinator, queue, or destination implementation in Release 1
shared:extensions-apiPlaceholder-only stable extension and permission contracts; no plugin installation/runtime in Release 1
shared:searchIndexing/query interfaces and common query behavior
android:appAndroid UI, lifecycle/background work, storage access framework integration
ios:appiOS UI, lifecycle/background tasks, security-scoped/bookmarked location handling if applicable

Keep module boundaries enforceable: platform apps call use cases; adapters implement ports; the domain layer does not import platform UI or destination SDKs.

Canonical data model

Live-preview editing model

Markdown is the canonical persisted and interchange representation, not the editor UI model. Each native app presents a structured, live-rendered document projection and sends semantic edits—such as “toggle bold for this selection,” “make this block a heading,” or “check this task”—to shared editing/domain logic. That logic updates the document and serializes valid Markdown when the note is saved.

  • Do not expose a raw-Markdown editor, syntax toggle, or Markdown control characters in normal note UI.
  • Preserve the user-visible selection, cursor, undo/redo behavior, and accessibility focus across structured edits.
  • Keep parsing and serialization deterministic so an unchanged rich document does not produce noisy Markdown/revision changes.
  • Treat unsupported or malformed Markdown from import/future sync as a recoverable compatibility case: preserve the source safely and render the supported subset without destructive rewriting.

Platform UIs own text-input integration and visual rendering. Shared code owns the canonical document transformations, Markdown codec contract, and conformance tests that verify rich-edit operations round-trip to Markdown correctly.

Note

FieldNotes
noteIdStable UUID/ULID generated locally
notebookIdOwning notebook; always set. Notes created outside a notebook use the built-in default notebook (see D-15)
pinnedAtNullable timestamp; non-null means the note is pinned (see D-16). Pinned notes list first, ordered by most recent edit. Changing pinnedAt is not an edit: it creates no revision and leaves updatedAt unchanged
titleStored separately from the body. Generated automatically (D-23): by an on-device language model when one is available, otherwise from the first line of the note
titleSourceHow the title was set: firstLine, generated or user. Lets the app avoid overwriting a title the user chose
markdownCurrent UTF-8 Markdown document body
createdAt, updatedAtUTC timestamps for display and future ordering hints
deletedAtTombstone timestamp; supports local recovery and future deletion synchronization
currentRevisionIdPoints to the current immutable revision
originDeviceIdNon-user-identifying install/device identity for diagnostics and conflict display

A link from one note to another is stored in the Markdown as a standard link whose destination is an app-defined address containing the target noteId (for example jotry://note/<noteId>), with the visible text as the link label. Because the target is addressed by ID, renames and moves do not break links. Shared code owns parsing and creating these links and keeps a local index of outgoing links per note so a missing or trashed target can be detected without scanning bodies. A link follows the target’s current title unless the user chose its label, which the address marks as jotry://note/<noteId>?label=custom (D-39). Shared code relabels links when a note is shown or opened for editing; the stored label catches up the next time the linking note is saved, so a rename creates no revisions in other notes. Exported files write a note link as a relative link to the target’s file, for example [Ideas](../personal/ideas--<noteId>.md); a link to a note that isn’t exported keeps its label as plain text. Import turns relative links to imported files back into note links (D-36). Backlinks (which notes link here) can be derived from the same index later, but are not a Release 1 requirement.

Notebook

FieldNotes
notebookIdStable UUID/ULID
nameUser-visible name, unique only if product rules require it
createdAt, updatedAt, archivedAtLifecycle timestamps
sortOrderUser-controlled order, represented in a future-sync-safe way
isDefaultTrue for the single built-in default notebook. It is hidden in the UI and cannot be renamed, archived or deleted (D-15)

There is no parent-notebook field: notebooks are flat and hold only notes (D-15).

Revision

Each revision is immutable. It records a complete snapshot, which is simpler and safer than an operation log or a delta chain (D-37). The changes shown in version history are diffed from snapshots when viewed and are not stored. If storage measurements show a need, unchanged bodies may be stored once and shared between revisions.

FieldNotes
revisionIdStable ID
noteIdParent note
parentRevisionIdPrevious known revision; enables a revision graph when conflicts occur
markdownSnapshotExact document content at this revision
titleSnapshot, notebookIdSnapshot, deletedAtSnapshotImportant state needed to recover the note faithfully
createdAt, authorDeviceId, reasonAudit/display data; reason examples: edit, restore, import, merge
contentHashDetect unchanged saves and validate transfers

Future sync metadata boundary

Release 1 stores no per-destination state, tokens, remote versions, or pending operations. When a destination is added, its metadata must live outside the Markdown document—keeping files portable and preventing sync internals from leaking into user content.

Parsed task items

Task items are not a second source of truth. Their checked state is part of the canonical Markdown serialization but is presented as a visual checkbox in the app. A parser/index may derive task-item data to support editor rendering, accessibility, and search:

Derived fieldNotes
noteId and source range/anchorLets the UI locate the task item in a particular note revision/current body
textHuman-readable task label
completedDerived from the serialized checkbox marker; never displayed as source syntax in the app
nestingLevelRepresents nested Markdown task lists

The toggle use case must target the current structured task node safely, update only its checked state, and reject/stage a refresh if the editor content changed before it applies. A successful toggle updates the full note through the normal transactional revision path.

Storage model

  • Use a local transactional database for notes, notebooks, revisions, and local search/index data. The chosen database is SQLite via SQLDelight (D-09). Add destination metadata and an outbox only when operational sync is implemented.
  • Store each note’s current body and revisions atomically with its metadata; a visible update must never exist without its revision.
  • Commit editor changes using a short debounce plus lifecycle flush. A save only creates a revision if the meaningful state changed.
  • Treat deletion as a tombstone, not immediate physical removal. A retention policy can later account for destination acknowledgements when sync exists.
  • Maintain a migration strategy from day one, including migration test fixtures for every released schema.

Markdown file representation

Markdown export follows D-42: one file per current note, named after its title, in a folder per notebook, with the default notebook’s notes at the root:

<export-root>/
  <Note title>.md
  <Notebook name>/
    <Note title>.md

Export always writes YAML front matter with portable identity and organization metadata only (note_id, notebook_id, notebook, title, created_at, updated_at, pinned). It never holds revision snapshots or authentication data. The user’s Markdown body is kept exactly below the front matter. Identity comes from note_id, not the file name, so renamed notes and repeated titles stay unambiguous.

The layout recommended here earlier (<notebook-slug>/<note-slug>--<noteId>.md) was replaced by D-42. Import still reads it. How folder sync stores identity (a note-to-file table rather than front matter) is planned in docs/spikes/folder-sync.md, not decided.

History and conflict rules

  1. A local edit creates a new immutable revision in one transaction.
  2. Checking or unchecking a task item is a local edit: it changes canonical Markdown and creates an immutable revision.
  3. A restore produces a new revision whose content equals the selected historic revision, preserving the current revision as its parent.

Future sync must add outbox operations and remote reconciliation without changing these local history guarantees. A remote revision that descends from the local current revision may fast-forward; divergent branches must be preserved rather than silently discarded. A destination without remote history may exchange only current documents while local history remains complete.

Security and privacy baseline

  • If/when sync is added, keep credentials and destination tokens in the platform secure-storage facility, never in Markdown files or logs.
  • Minimize collected telemetry; make diagnostics opt-in and redact note content.
  • Define encrypted transport and end-to-end encryption separately for any future cloud sync design.
  • Clearly explain what a future destination can read, write, retain, and recover before it is enabled.