Architecture and Data Model
Recommended shape
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
| Module | Responsibility |
|---|---|
shared:domain | Entities, validation, use cases, conflict and revision rules |
shared:data | Repository implementations, serialization, database mappings, migrations |
shared:sync-contracts | Placeholder-only capability and adapter contracts; no coordinator, queue, or destination implementation in Release 1 |
shared:extensions-api | Placeholder-only stable extension and permission contracts; no plugin installation/runtime in Release 1 |
shared:search | Indexing/query interfaces and common query behavior |
android:app | Android UI, lifecycle/background work, storage access framework integration |
ios:app | iOS 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
| Field | Notes |
|---|---|
noteId | Stable UUID/ULID generated locally |
notebookId | Owning notebook; always set. Notes created outside a notebook use the built-in default notebook (see D-15) |
pinnedAt | Nullable 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 |
title | Stored 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 |
titleSource | How the title was set: firstLine, generated or user. Lets the app avoid overwriting a title the user chose |
markdown | Current UTF-8 Markdown document body |
createdAt, updatedAt | UTC timestamps for display and future ordering hints |
deletedAt | Tombstone timestamp; supports local recovery and future deletion synchronization |
currentRevisionId | Points to the current immutable revision |
originDeviceId | Non-user-identifying install/device identity for diagnostics and conflict display |
Note links
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
| Field | Notes |
|---|---|
notebookId | Stable UUID/ULID |
name | User-visible name, unique only if product rules require it |
createdAt, updatedAt, archivedAt | Lifecycle timestamps |
sortOrder | User-controlled order, represented in a future-sync-safe way |
isDefault | True 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.
| Field | Notes |
|---|---|
revisionId | Stable ID |
noteId | Parent note |
parentRevisionId | Previous known revision; enables a revision graph when conflicts occur |
markdownSnapshot | Exact document content at this revision |
titleSnapshot, notebookIdSnapshot, deletedAtSnapshot | Important state needed to recover the note faithfully |
createdAt, authorDeviceId, reason | Audit/display data; reason examples: edit, restore, import, merge |
contentHash | Detect 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 field | Notes |
|---|---|
noteId and source range/anchor | Lets the UI locate the task item in a particular note revision/current body |
text | Human-readable task label |
completed | Derived from the serialized checkbox marker; never displayed as source syntax in the app |
nestingLevel | Represents 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
- A local edit creates a new immutable revision in one transaction.
- Checking or unchecking a task item is a local edit: it changes canonical Markdown and creates an immutable revision.
- 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.