Data model
The domain model is plain Kotlin data classes in
core/…/domain/model/, with
no database or Android imports. How these types become columns is in Database,
how they become JSON in Backup format and Sync wire format.
The ideas behind them are in Tasks, projects and tags and
Recurrence.
Relationships
Section titled “Relationships”erDiagram
PROJECT |o--o{ PROJECT : "parentId (one level)"
PROJECT |o--o{ TASK : "projectId (null = Inbox)"
PROJECT ||--o{ SECTION : "projectId"
SECTION |o--o{ TASK : "sectionId"
TASK |o--o{ TASK : "parentId (subtask)"
TASK |o--o{ TASK : "spawnedFromId (successor)"
TAG }o--o{ TASK : "tagIds, packed on the task"
TASK ||--o{ ATTACHMENT : "taskId"
Every link is an id string, and none is enforced by the database except
attachmentRow.taskId (see Database). A link that names nothing
is tolerated and repaired on read: a task whose project is gone lands in the Inbox, a tag id no
live tag answers to is dropped by CadenceUiState.tagsOf.
Common conventions
Section titled “Common conventions”| Convention | Applies to | Rule |
|---|---|---|
id |
Task, Project, Section, Tag, Attachment |
A UUIDv7 string. Defaults to ""; CadenceRepository mints one (UuidV7.random()) when a new row is saved with a blank id. Storage never assigns ids. |
| Successor id | Task |
Derived, not random: UuidV7.successorId(spawnedFromId, occurrenceDate), so two devices completing the same occurrence offline produce the same id. |
updatedAt |
all synced types | Last write, local or merged in; conflicts resolve on it (greater wins, ties keep what is stored). Stamped by CadenceRepository.now(), truncated to milliseconds. Default Instant.EPOCH. |
deletedAt |
all synced types | Tombstone. Non-null means deleted; every ordinary read hides it. Default null. |
sortOrder |
all list types | Manual position, dense from 0 within its bucket. A new row lands at max + 1. |
| Field | Type | Default | Meaning |
|---|---|---|---|
id |
String |
"" |
UUIDv7, minted on first save. |
title |
String |
— (required) | The task’s text. |
notes |
String? |
null |
Free text. |
priority |
Priority |
Priority.DEFAULT (P3) |
Importance; sorts before the due date. |
projectId |
String? |
null |
Project the task is filed under; null is the Inbox. |
sectionId |
String? |
null |
Section of projectId the task is grouped under; null is the ungrouped band. Cleared when the task moves to another project. |
tagIds |
List<String> |
emptyList() |
Tag ids in the order applied. Membership lives here; there is no join table. |
parentId |
String? |
null |
The task this one is a step of. One level deep. |
spawnedFromId |
String? |
null |
The finished occurrence whose completion inserted this row. |
dueDate |
LocalDate? |
null |
Due day. |
dueTime |
LocalTime? |
null |
Due time of day. Reminder leads count back from it. |
reminderTime |
LocalTime? |
null |
An exact reminder time on the due day, independent of dueTime. |
completedAt |
Instant? |
null |
When it was completed; null while open. |
createdAt |
Instant |
Instant.EPOCH |
Creation time. |
sortOrder |
Int |
0 |
Manual position within its project (or the Inbox). Subtasks are ordered within their parent. |
recurrence |
RecurrenceRule? |
null |
The repeat rule, or null. |
updatedAt |
Instant |
Instant.EPOCH |
See conventions. |
deletedAt |
Instant? |
null |
See conventions. |
Derived members: isDone (completedAt != null), isInbox (projectId == null), isSubtask
(parentId != null), isOverdue(today) (open, dated, before today), isDueOn(day).
Helpers in the same file: List<Task>.withoutSupersededOccurrences() drops a completed
occurrence that some other row names as its spawnedFromId; SubtaskProgress(done, total) with
fraction and isComplete.
Project
Section titled “Project”| Field | Type | Default | Meaning |
|---|---|---|---|
id |
String |
"" |
UUIDv7. |
name |
String |
— (required) | Display name; must not be blank (upsertProject refuses it). |
colorHex |
String |
"#006A60" |
Colour as #RRGGBB. |
parentId |
String? |
null |
Parent project. |
sortOrder |
Int |
0 |
Position among the projects with the same parentId. |
updatedAt |
Instant |
Instant.EPOCH |
See conventions. |
deletedAt |
Instant? |
null |
See conventions. |
Derived: isSubproject (parentId != null). Helpers: ProjectTreeNode(project, children),
List<Project>.toTree() (one level, orphans dropped), projectPath(project, all) ("Home / Finance").
Section
Section titled “Section”Section.kt — a
heading inside one project’s list. Never nested, never project-less.
| Field | Type | Default | Meaning |
|---|---|---|---|
id |
String |
"" |
UUIDv7. |
projectId |
String |
— (required) | The project this section groups. |
name |
String |
— (required) | Heading text. |
sortOrder |
Int |
0 |
Position among the project’s sections. |
updatedAt |
Instant |
Instant.EPOCH |
See conventions. |
deletedAt |
Instant? |
null |
See conventions. |
Tag.kt — identity
only; which tasks wear it is Task.tagIds.
| Field | Type | Default | Meaning |
|---|---|---|---|
id |
String |
"" |
UUIDv7. |
name |
String |
— (required) | Display name. Unique case-insensitively on one device (a validation, not a constraint). |
colorHex |
String |
"#3E6373" |
Colour as #RRGGBB. |
sortOrder |
Int |
0 |
Position in the one flat tag list. |
updatedAt |
Instant |
Instant.EPOCH |
See conventions. |
deletedAt |
Instant? |
null |
See conventions. |
Derived: handle — name with spaces removed ("Deep Work" → DeepWork), what @… matches.
List<Tag>.matchingHandle(typed) tries, case-insensitively: exact name, then exact handle, then a
handle prefix — the prefix pass answers only when exactly one tag matches.
Attachment
Section titled “Attachment”Attachment.kt
— local to the device: never synced, not in the backup file. See
Attachments.
| Field | Type | Default | Meaning |
|---|---|---|---|
id |
String |
"" |
UUIDv7. |
taskId |
String |
— (required) | The task it is filed on. |
kind |
AttachmentKind |
— (required) | LINK or FILE. |
name |
String |
— (required) | File name, or the link’s title. |
mimeType |
String |
— (required) | The blob’s media type; "text/uri-list" for a LINK. |
sha256 |
String? |
null |
Lowercase hex SHA-256 of the bytes — the blob’s name. null for a LINK. |
sizeBytes |
Long |
0L |
Size of the blob. |
url |
String? |
null |
The URL for a LINK; null for a FILE. |
createdAt |
Instant |
Instant.EPOCH |
When it was added. |
sortOrder |
Int |
0 |
Position on the task. |
Derived: isImage (mimeType.startsWith("image/")). Limits, in
CadenceRepository.kt:
MAX_ATTACHMENT_BYTES = 25 MiB, MAX_ATTACHMENTS_PER_TASK = 20. (MAX_SUBTASKS_PER_PARENT = 100
lives beside them.)
RecurrenceRule
Section titled “RecurrenceRule”Recurrence.kt.
Dates are computed by RecurrenceEngine.
| Field | Type | Default | Meaning |
|---|---|---|---|
mode |
RecurrenceMode |
SCHEDULE |
Calendar rule, or anchored to the completion day. |
interval |
Int |
1 |
Every n units. Read as at least 1 everywhere. |
unit |
RecurrenceUnit |
WEEK |
Day, week, month or year. |
daysOfWeek |
Set<DayOfWeek> |
emptySet() |
Weekly rules: which days. Empty means “same weekday, every n weeks”. |
monthlyMode |
MonthlyMode |
DAY_OF_MONTH |
Monthly and yearly rules: how the day is picked. |
dayOfMonth |
Int? |
null |
For DAY_OF_MONTH; null reads as 1. Clamped to the month’s length. |
nthWeek |
Int? |
null |
For NTH_WEEKDAY: 1–4, and 5 (or more) means “the last one”. null reads as 1. |
nthDayOfWeek |
DayOfWeek? |
null |
For NTH_WEEKDAY; null reads as Monday. |
RecurrenceRule.fromNames(…) builds a rule from enum names — what the backup file and the wire
carry. It is lenient: an unknown name falls back to the field default, an unknown weekday is
dropped, interval is clamped to at least 1.
RecurrenceMode
Section titled “RecurrenceMode”| Value | Next date is computed from |
|---|---|
SCHEDULE |
The previous due date, walking the calendar. Completing late hands over to the first occurrence not before the completion day. |
AFTER_COMPLETION |
The day the task was completed, plus interval × unit. |
RecurrenceUnit
Section titled “RecurrenceUnit”DAY, WEEK, MONTH, YEAR.
MonthlyMode
Section titled “MonthlyMode”| Value | Day chosen in a month |
|---|---|
DAY_OF_MONTH |
dayOfMonth (default 1), clamped to the month’s last day. |
LAST_DAY |
The last day of the month. |
LAST_WEEKDAY |
The last Monday–Friday of the month. |
NTH_WEEKDAY |
The nthWeek-th nthDayOfWeek; nthWeek ≥ 5 is the last one. |
A yearly rule with DAY_OF_MONTH and no dayOfMonth simply adds interval years.
Priority
Section titled “Priority”| Value | level |
shortLabel |
filledBars |
English name | German name |
|---|---|---|---|---|---|
P1 |
1 | P1 |
3 | Critical | Kritisch |
P2 |
2 | P2 |
2 | High | Hoch |
P3 |
3 | P3 |
1 | Normal | Normal |
P4 |
4 | P4 |
0 (drawn as a dash) | Low | Niedrig |
Priority.DEFAULT is P3. Priority.fromLevel(n) returns DEFAULT for any level outside 1–4.
The names come from string resources via ui/format/PriorityLabels.kt.
CadenceSettings
Section titled “CadenceSettings”ui/settings/SettingsStore.kt
— per device, never synced. Where each shell stores them is in Configuration.
| Field | Type | Class default | Meaning |
|---|---|---|---|
theme |
ThemeChoice |
SYSTEM |
SYSTEM, LIGHT, DARK. |
density |
Density |
COMFORTABLE |
COMFORTABLE (64 dp rows) or COMPACT (52 dp rows). |
sortMode |
SortMode |
IMPORTANCE |
IMPORTANCE, DATE, MANUAL. |
showCompleted |
Boolean |
true |
Whether completed tasks stay in the lists. |
remindersEnabled |
Boolean |
true |
Whether this device fires reminders. The shell’s store decides the real default: on for Android, off for the desktop. |
reminderLeadMinutes |
List<Int> |
emptyList() |
One notification per entry, that many minutes before dueTime. |
Sync and desktop state
Section titled “Sync and desktop state”Two more records are persisted but are not domain data.
SyncState (data/sync/SyncStore.kt),
stored in syncStateRow:
| Field | Type | Default | Meaning |
|---|---|---|---|
session |
String? |
null |
Serialised NeonSession (accessToken, sessionCookie, email); null when signed out. |
taskCursor, projectCursor, sectionCursor, tagCursor |
String? |
null |
Per-table pull cursor: the newest server_updated_at pulled, as ISO-8601 text (the server’s clock). |
pushWatermark |
Instant |
Instant.EPOCH |
The newest local updatedAt pushed (this device’s clock). |
lastSyncedAt |
Instant? |
null |
When the last round finished. |
lastSweepAt |
Instant? |
null |
When tombstones were last collected. |
DesktopWorkspace (DesktopWorkspaceStore.kt),
desktop only, in workspace.json:
| Field | Type | Default | Clamp |
|---|---|---|---|
sidebarWidth |
Float (dp) |
268f |
200–420 |
sidebarCollapsed |
Boolean |
false |
— |
collapsedProjects |
Set<String> |
emptySet() |
— |
windowWidth |
Float (dp) |
1180f |
at least 640 |
windowHeight |
Float (dp) |
820f |
at least 480 |
windowX, windowY |
Float? |
null (the platform places the window) |
a negative coordinate is dropped back to null |
maximized |
Boolean |
false |
— |