What holds for every app and package of this repository is in the [root AGENTS.md](../../AGENTS.md); it applies here in full, and Learn's patterns (`apps/learn/AGENTS.md`) wherever Todo needs the same thing.
- **A shell on the shared packages** (decided 2026-10-19; what Todo is to do and how it looks: ADR 0033, decided 2026-11-01): signing in, the instances, the account and the look work; since 1 Nov 2026 a person's projects are kept and synced with `serverpod_offline_sync`, every row sealed (ADR 0033, D5, D6): `content_key` and `project` on the server (`deleteTodoData` deletes a person's space with them), and on the device `TodoKeys` (Train's `TrainKeys` copied with Todo's own key IDs, `todo1.…`; to be shared in halkyon_app once Train's 0.1.1 is out), `SealedRows` (`lib/src/core/sealed/`: a table's rows sealed for their table and row, under which `ProjectRepository`, the Inbox under a row ID keyed by the account, made once as the person's data opens, never deleted, `SectionRepository`, `TaskRepository`, a task deleted with its sub-tasks, and `LabelRepository`), one sync gateway per table, and `TodoPerson` opening the person's sync database, its `SyncService` and all of them, as Train's; a project or a section is deleted with its tasks (`TodoPerson.deleteProject`, `deleteSection`). A task's date is a day with an optional wall-clock time and duration, no time zone (`TaskDate`), and its recurrence (`Repeat`, ADR 0033, D7: `TaskRepository.complete` moves a recurring task on, reopens its sub-tasks and returns what it changed for `restore` to undo; picked through `pickRepeat`) and its reminders, minutes before its time and at times of their own (D5, D18: `reminders` and `reminderTimes`, `ReminderTime`; a task's payload keeps the fields a newer version wrote, `TaskContent.unknown`, so its schema stays 1; `planReminders` plans them, `ReminderScheduler` under the person's app hands them to `ReminderGateway`, `SystemReminderGateway` on `flutter_local_notifications`, the system scheduling them but on Linux, where the running app shows them; `TodoPerson.dispose` cancels them; the permission to notify asked wherever a reminder is planned, once an app start, and Settings' Reminders saying while notifications are off, with Turn on, `NotificationAccess`, D31; Android's manifest declares the boot receiver and exact alarms, `USE_EXACT_ALARM` and `SCHEDULE_EXACT_ALARM` up to API 32, `test/android/exact_alarms_test.dart`, the build desugars). A task's locations (D32): `TaskLocation` in its sealed payload as `locations`, several being alternatives, each reminding on arriving, on leaving or not; `lib/src/feature/location/`: `showLocationPicker` (a map with a pin fixed in its middle, of fixed size above a sheet pulled up over it, a `SidePane` beside it in a wider window, what is around it named, a compass, and a search by city or district and name on a screen of its own, `PlaceSearchScreen`, over `PlaceSearchGateway`, `TodoDependencies.places`, the instance's seam, komoot's public Photon in a debug build with `--dart-define=TODO_PUBLIC_PHOTON=true` alone; results as a list and pins on Train's OpenFreeMap map, `mapTiles`, none in tests; a pin placed by hand), `+` in quick add, Where in the sheet; reminders on Android alone, `planLocationWatches`, `ReminderScheduler`, `LocationReminderGateway` over `LocationReminders.kt` (a foreground service's fixes, no Google services; a two-minute dwell, `LocationDwell`, its JUnit tests run with `./gradlew :app:testDebugUnitTest`, not in CI; `adb logcat -s HalkyonLocation` in a debug build), `LocationReminderAccess` as `NotificationAccess`. A debug build is `app.halkyon.todo.debug`, *Todo Debug*, beside the release. Packages: `halkyon_todo_server`, `halkyon_todo_client` (generated, never edited), `halkyon_todo_flutter`; app ID `app.halkyon.todo` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`).
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`todoBase()`, `lib/src/todo_base.dart`): its basics say it serves `todo` (never change it once apps use it), sign-in mails name Halkyon Todo, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteTodoData]`, `lib/src/todo_data.dart`), people's files kept in the module's store (`files: todoFiles`, `lib/src/attachment/todo_files.dart`: the `attachment` rows, sealed files alone, Halkyon's ADR 0043). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Todo holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`todoBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode), and the files no attachment names any more daily (`FilePruningFutureCall`). Session logs are never persisted, in any run mode. Migrations (2026-10-19): the initial one (Serverpod's tables, its auth modules' and the module's `email_identity`, `email_code_request`), `profiles` (`public_profile`), `deletion-notices` (`account_deletion_notice`), and since 1 Nov 2026 `projects` (`content_key`, `project` and the sync engine's tables) and `tasks` (`section`, `task`, `label`), `settings` (`todo_settings`), `filters` (`saved_filter`, D35), `comments` (`task_comment`, D36), `file-store` (halkyon_base's `file_allowance`) and `attachments` (`attachment`, Halkyon's ADR 0043). **Places** (D32): `SearchEndpoint` (`lib/src/search/`, copied from Train's: `offered`, `places`, `Place`, `PlacePoint`, `SearchRefused`) finds them, and what is around a point (`nearby`), through the instance's Photon (`PHOTON_URL`, `deploy/compose.todo-search.yaml`; none: not offered), for those signed in, nothing kept or logged, a shop's car park and charger left out. **Forecasts** (D40): `WeatherEndpoint` (`lib/src/weather/`, Train's ADR 0036 copied: `offered`, `forecast`, `ForecastHour`, `WeatherRefused`, `Forecasts`) answers a place's hours for the next 16 days from Open-Meteo, the place rounded to about a kilometre, for those signed in, nothing kept or logged, `WEATHER=off` and `OPEN_METEO_API_KEY` as Train's. Tests (`test/integration/todo_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Todo's frame (`todoFrame`: "Todo" under "Halkyon", its icon, `todo.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'todo')`; `TodoDependencies` (`AppDependencies<TodoPerson>`) on one instance, `TodoPerson` (empty) per person; a person's app in Todo's frame (ADR 0033, D1 to D4, since 1 Nov 2026), as Train's: go_router (`buildTodoRouter`, `TodoPaths`) with a `StatefulShellRoute`, halkyon_ui's `NavigationFrame` (`TodoShell`) with the tabs Inbox, Today (where it opens), Upcoming and Browse (D12; Browse's first row opens every Halkyon app's Profile over the tabs, halkyon_app's `HalkyonProfile` at `TodoPaths.profile`, decided 2026-11-02, as Todoist keeps the account there), (`lib/src/tabs/`; ADR 0033, D8, since 1 Nov 2026): Today (overdue first with Reschedule, then today's; or, as its Display sets, `todayDisplay`, `TodayView` in the person's sealed settings, D30, today's hours, `HourGrid` with one day, beside the Plan, `PlanPanel`: Overdue, To plan and Time-blocked, filtered by project for the session, its rows dragged onto an hour and the day's blocks back onto it through `moveTask`; a panel from a medium width of Today's own on, shown or hidden as remembered, on a phone a sheet over the day's foot, part of the screen, lowering while a row is held), Upcoming (overdue, then the next 28 days, empty ones muted), Inbox and a project's screen (`ProjectScreen`, from Browse: its tasks above and in its sections, shown as its Display sets (D17: `ProjectView` in the project's sealed payload, `DisplayButton` and `showDisplaySheet` in `display_sheet.dart`; its layout, completed tasks, grouping, sorting, filters, applied by `ProjectTasks` with `groupTasks`; laid out as a board, `ProjectBoard`, `project_board.dart`: a column a section, paged below a medium width, side by side from it; as a table, `ProjectTable`, `project_table.dart`, D36: from 720 pixels of its width on, a list below, offered from expanded windows on; its columns in `ProjectView.columns`, in order, Task moving among them, `showTableColumns` turning them and the task cell's `TaskDetail`s on and off, a heading's menu filtering, sorting, moving and hiding its column, sideways scrolling where they don't fit; as a calendar, `ProjectCalendarView`: Upcoming's `DayAgenda` over `calendarOf`, with *No date* and future occurrences of repeating tasks, and from 720 pixels of its width on a month, `MonthGrid` over `monthOf`), sections added, renamed and deleted, the project edited (`editProject`, `project_editor.dart`, as Todoist's Add project: name, description, colour, parent, favourite, layout, D28; its default Display, `defaultView`, what Display's Reset returns to) and deleted, with its sub-projects), a calendar's feed linked to a project (D39, `lib/src/feature/calendar/`: the address in the project's sealed payload, `calendarUrl`, written in its editor; `CalendarCubit` following the projects, fetching each feed by the device, `HttpCalendarFeeds`, as the app opens, hourly and on a pull, the last copy in the person's folder, `FileCalendarStore`; `readIcal` reading a feed's events, recurrence by `rrule`; `FakeCalendarFeeds` in `test/support/`), its events read-only beside the tasks (`EventTile`, `showEvent`, `eventsOn`, `eventsFrom`) in Today's list, the agenda, the hour grids (`layOutItems`, `_EventBlock`), the month (`_EventLine`) and saved filters of days (`filterTakesEvent`), the weather (D40, `lib/src/feature/weather/`: `ForecastCubit` following the places `forecastPlaces` names, the person's own from Settings, `TodoSettings.weatherPlace`, else each open dated task's first place where it needs weather, through `WeatherGateway`, `InstanceWeather` on the server's `weather`, `TodoDependencies.weather`, `FakeWeather` in `test/support/`, asked as the app opens, hourly and on a pull, held in memory; a task's `WeatherNeeds` in its sealed payload as `weather`, `missingOn` judging them over its hours, `weatherSpan`; its row's mark, `TaskTile.weatherMarkKey`, its sheet's Weather row, `pickWeatherNeed
- **The icon** is Lucide's `list-todo` fading from cobalt through violet into strawberry, its backdrop (ADR 0031, D5), drawn with the other apps' by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py todo` from the root): `assets/icon/halkyon_todo_icon.png`, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. `registerLucideLicense` puts the notice in the licence list.
- **`serverpod start`** runs in `apps/todo/halkyon_todo_server` (it launches the app from `lib/driver.dart` on Linux, `device: linux`, as Todo has no web build); `.mcp.json`'s `--server-dir` names it when a session works on Todo. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Learn's and Todo's development servers use the same ports: run one at a time.
- **Deployed** (ADR 0033, step 5) beside learn.halkyon.eu's server once `deploy/.env` names `deploy/compose.todo.yaml`: at todo.halkyon.eu (`config/production.yaml`, the image `Dockerfile` as Train's), a member of Learn's register (named to it by `INSTANCE_MEMBER_TODO`), its database `halkyon_todo` in the instance's PostgreSQL; `redeploy.sh` and `backup.sh` take Todo's server and database in only where it is switched on, and the operator's steps are `deploy/sidecars/README.md`, *Todo*.
- **Releases** (ADR 0033, step 5) come from `dart run tool/release.dart todo` on the developer's laptop, as Learn's and Train's (root AGENTS.md, Learn's *Releases*): `apps/todo/release.dart`, the APK and the snap for todo.halkyon.eu, on code.halkyon.app, the APK in the project's F-Droid repository (`tool/release/fdroid/metadata/app.halkyon.todo.yml`, `NonFreeNet`), the snap to the Store's edge channel; signed with Todo's own key (`~/.local/share/halkyon-todo/android-release.p12`, alias `halkyon-todo`, pinned in `tool/release/todo-android-certificate.sha256`; `android/app/build.gradle.kts` signs with it when the release tool hands it over, else with the debug key). The snap's recipe is `apps/todo/snap/` (its icon from `tool/app_icons.py`): snapcraft builds one recipe a folder and the root's `snap/` is Learn's, so the release tool builds Todo's in a clone of the repository at the tag kept in `~/.cache/halkyon-release/snap/halkyon-todo`, Todo's folder in place of the clone's `snap/` but for the shared `snap/local/` (`SnapRelease.projectPath`, `_snapProject`).