There are hundreds of planners and I'm building another one. It's called Dispatch, it runs on my Mac and my phone, and right now it barely works — which is the honest reason to write about it while the decisions are still visible.
Partly because nothing quite fits. Every planner I've used is opinionated about how a day should be shaped, and I keep bending my week around the app instead of the other way round.
Mostly, though, because building it teaches more than using it. Swift, SwiftData, a real database with real access rules — this is a project with enough surface area to be wrong in interesting ways.
The useful constraint is that it's for one person. No paid Apple Developer membership, so it installs on my own machines and nowhere else. No App Store review, no onboarding flow, no settings screen for other people's preferences. Every decision only has to be right for me, and when it isn't I'm the only one who has to live with it.
The core rule: a write never waits on the network. Typing a task saves to SwiftData on the device immediately, and syncing to Supabase happens afterwards, out of the way.
That sounds obvious until you build the alternative by accident. If the cloud is the source of truth, every airplane, elevator and dead Wi-Fi network becomes a bug report. So realtime updates are treated as a hint to refresh, not as the mechanism data recovery depends on — if the socket never fires, the app still catches up on its own.
How a change travels through DispatchЯк зміна проходить через Dispatch
Every change to the data model goes through a single object, WorkspaceStore. Nothing writes to the database directly, not even the small stuff.
The reason is that a task edit isn't one thing happening. Revision history, undo state, the snapshot the widget reads, and the queue waiting to sync all have to agree about what just happened. Two write paths means two chances for them to disagree, and the disagreements surface days later as a widget showing a task you deleted.
The bill for sync arrives when you add a single field. One new property means updating the SwiftData model, the cloud data-transfer object, the encode and decode mapping, a Supabase migration, and the row-level security rule that governs it — five places, in one change, or the field half-exists. Nobody tells you sync costs that up front.
The part I actually care about: letting an agent in#
Dispatch exposes an MCP interface, so an AI agent can read and change my plan. That's the feature I'm most interested in and the one I'm most careful with, because "an assistant that can edit your tasks" and "a program with your account" are one sloppy design decision apart.
The rules I'm holding myself to:
An agent gets a scope, never my account. Explicit, short-lived, revocable, and narrower than my full workspace whenever it can be. Handing over one project doesn't quietly widen into everything.
Every write validates that scope and gets logged. If I can't tell afterwards which agent did what, the log isn't doing its job.
Writes carry the revision they expect. If the record moved since the agent read it, the write fails as a conflict instead of flattening whatever happened in between.
Retries carry an idempotency key. A network hiccup shouldn't quietly create the same task twice.
No permanent-delete tool. Not until deletion is recoverable and I've explicitly turned it on.
A key in the client is not a lock
The publishable Supabase key ships inside the app, so anyone with the app has it. It identifies the project; it authorises nothing. What actually protects the data is row-level security plus the signed-in user's token — the rules live in the database, where a client can't argue with them.
What Security+ looks like when it's not on a flashcard#
Reading the exam objectives and then writing this were oddly the same exercise. Least privilege stopped being a definition the moment I had to decide what a scope should not include. Non-repudiation is just the activity log. Fail-safe defaults is the conflict that stops instead of overwriting.
I don't think I understood any of those three until something I wrote could get them wrong.
Early. The Mac and iPhone versions share almost all their interface, sync works often enough to be annoying when it doesn't, and the agent side is more design than code. watchOS is a someday.
I'll keep the project page updated as it gets less broken.
Планерів існують сотні, а я роблю ще один. Він називається Dispatch, працює на моєму Mac і телефоні, і зараз ледве живий — що і є чесною причиною написати про нього саме тепер, поки рішення ще на видноті.
Частково тому, що жоден не пасує повністю. Кожен планер, яким я користувався, має власну думку про те, як має виглядати день, і я щоразу підлаштовую свій тиждень під застосунок, а не навпаки.
Але головно тому, що робити його вчить більше, ніж ним користуватися. Swift, SwiftData, справжня база даних зі справжніми правилами доступу — тут достатньо простору, щоб помилятися цікаво.
Корисне обмеження — застосунок для однієї людини. Платного членства Apple Developer немає, тож він ставиться на мої машини й більше нікуди. Жодного рев'ю в App Store, жодного онбордингу, жодного екрана налаштувань під чужі вподобання. Кожне рішення має бути правильним лише для мене, а коли воно неправильне — жити з ним теж тільки мені.
Основне правило: запис ніколи не чекає на мережу. Набрана задача одразу зберігається у SwiftData на пристрої, а синхронізація з Supabase відбувається потім, десь осторонь.
Це звучить очевидно, поки випадково не збудуєш протилежне. Якщо джерелом правди є хмара, то кожен літак, ліфт і мертвий Wi-Fi стають баг-репортом. Тому realtime-оновлення тут — це підказка оновитися, а не механізм, на якому тримається відновлення даних: навіть якщо сокет так і не спрацює, застосунок дожене все сам.
How a change travels through DispatchЯк зміна проходить через Dispatch
Будь-яка зміна моделі даних іде через один об'єкт — WorkspaceStore. Ніщо не пише в базу напряму, навіть найдрібніше.
Причина в тому, що редагування задачі — це не одна подія. Історія ревізій, стан скасування, знімок, який читає віджет, і черга на синхронізацію мусять однаково розуміти, що щойно сталося. Два шляхи запису — це два шанси розійтися, а розбіжності випливають днями пізніше у вигляді віджета, який показує задачу, яку ти видалив.
Рахунок за синхронізацію приходить тоді, коли додаєш одне-єдине поле. Одна нова властивість означає оновити модель SwiftData, хмарний DTO, мапінг кодування й декодування, міграцію Supabase і правило row-level security, яке ним керує — п'ять місць, в одній зміні, інакше поле існуватиме наполовину. Про цю ціну синхронізації наперед ніхто не попереджає.
Частина, яка мене справді цікавить: пустити агента#
Dispatch має інтерфейс MCP, тож AI-агент може читати й змінювати мій план. Це водночас функція, яка мені найцікавіша, і та, з якою я найобережніший, бо «асистент, що редагує твої задачі» і «програма з твоїм акаунтом» відділені одним недбалим рішенням.
Правила, яких я себе тримаю:
Агент отримує дозвіл, а не мій акаунт. Явний, короткочасний, відкличний і якомога вужчий за весь мій робочий простір. Передача одного проєкту не має тихо розростатися до всього.
Кожен запис перевіряє цей дозвіл і потрапляє в журнал. Якщо потім не можна сказати, який агент що зробив, — журнал не виконує своєї роботи.
Записи несуть очікувану ревізію. Якщо запис змінився після того, як агент його прочитав, операція падає як конфлікт, а не затирає те, що сталося тим часом.
Повторні спроби несуть ключ ідемпотентності. Збій мережі не має тихо створювати ту саму задачу двічі.
Жодного інструмента остаточного видалення. Доки видалення не стане оборотним і доки я його явно не увімкну.
Ключ у клієнті — це не замок
Публічний ключ Supabase їде всередині застосунку, тож він є в кожного, хто має застосунок. Він ідентифікує проєкт і не авторизує нічого. Дані насправді захищають row-level security та токен авторизованого користувача — правила живуть у базі, де клієнт не може з ними сперечатися.
Читати цілі іспиту і писати це виявилося на диво тією самою вправою. Принцип найменших привілеїв перестав бути визначенням тієї миті, коли довелося вирішувати, чого дозвіл не має включати. Неспростовність — це просто журнал активності. Безпечні значення за замовчуванням — це конфлікт, який зупиняється замість перезапису.
Здається, я не розумів жодного з цих трьох, поки щось написане мною не змогло їх порушити.
На ранній стадії. Версії для Mac та iPhone майже повністю ділять інтерфейс, синхронізація працює достатньо часто, щоб дратувати, коли не працює, а частина з агентами — поки більше дизайн, ніж код. watchOS — колись.
Я оновлюватиму сторінку проєкту, поки воно ставатиме менш зламаним.