Architecture
Built on decisions, not defaults
Every major architectural choice has a written rationale. Here is what the system is made of and why each component was selected over its alternatives.
▤
SQLiteData / GRDB
Why
Full SQL with no limitations: JOINs, aggregates, window functions, FTS5 full-text search, STRICT mode type safety. PowerSync and SwiftData both had unacceptable constraints; GRDB has 10+ years of production use.
One local SQLite database stores tasks, notes, projects, areas, routines, and note bodies. Every write is a transaction. Every query is a prepared statement. FTS5 enables fast search across all notes without a separate index service.
SELECT * FROM tasks
WHERE planAt < date('now', '+1 day')
AND status = 'todo'
ORDER BY priority DESC, planAt ASC
☁
CloudKit Private Database
Why
$0 backend cost — all data lives in the user's private iCloud database. No Supabase billing, no PowerSync monthly fee, no per-request charges. CKSyncEngine handles conflict resolution, push delivery, and retry with backoff.
End-to-end encrypted by default. CloudKit fields can never be removed after production deployment — the schema was designed upfront for that immutability. Every field is optional to handle CKSyncEngine conflict scenarios safely.
⚓
Markdown Anchors
Why
Tasks embedded in notes need stable identity that survives external editing, copy-paste, and round-trips through other editors or LLM agents. HTML comments are invisible in rendered markdown but machine-readable.
UUID anchors carry compact metadata: priority, scheduled date, deadline, duration, tags. Stripped UUIDs create new tasks rather than silently corrupting existing records. The task_note_anchors table persists source context across app restarts.
- [ ] Follow up <!-- task:550e8400
priority:high plan:2026-06-15
tags:design -->
◎
Apple Reminders Projection
Why
Users already live in Reminders. DeftTask can sync any task to a Reminder — but Reminders is a read projection, not the source of truth. Deleting a Reminder disables sync; it never cascades to delete the DeftTask task.
Three-way shadow merge: DeftTask wins on conflicts. The task_external_links table tracks the 1:1 binding and a JSON shadow of the last-synced field set, enabling reliable change detection without polling all fields.
⟳
Echo Suppression
Why
Bidirectional sync across three surfaces (SQLite ↔ .md files ↔ Apple Reminders) can loop. No single edit may traverse more than one directed edge synchronously — three independent suppression mechanisms enforce this invariant.
Write token tagging prevents the file watcher from re-ingesting self-writes. An origin flag on the DB observer prevents re-entrancy on file-sourced changes. CloudKit's _isSynchronizingChanges guard prevents blocking the sync engine thread.
WriteTokenFileWriter.write()
→ PendingWriteTokenRegistry.register(token)
→ NoteFileWatcher.isTagged() → skip
⇄
Always-On Workspace Sync
Why
LLM agents and external tools need file-level task access without an API or MCP server. Making .md files a first-class sync target means any tool that can read and write files can manage DeftTask tasks.
TaskFileIndex maps UUIDs to file paths in memory. NoteFileWatcher detects external edits using DispatchSource filesystem events (one source per directory). TaskDBObserver renders DB changes back to files within 300ms of a GRDB write observation.
File edit in Vim → NoteFileWatcher
→ WorkspaceTaskSyncService
→ GRDB upsert
→ CloudKit → other devices
Architecture decisions documented in version-controlled ADRs · ADR-007 through ADR-011