One last to-do list
Every application framework eventually teaches you to build a to-do list. Most stop when a checkbox survives a page refresh.
Nomos will build one last to-do list and refuse to stop there.
We will begin with lists, items and a tick button because those nouns need no explanation. Then we will use that small domain to tackle the parts ordinary tutorials leave out: durable identity, individual history, shared authority, offline work, deterministic convergence, schema evolution, replay and recovery.
The aim is the most sophisticated to-do list in the world—not because to-dos deserve enterprise ceremony, but because everybody can see exactly what each Nomos superpower buys us.
The cliché is the point
Section titled “The cliché is the point”Engineers have built this example in every language and framework they have used. That shared experience is valuable: everybody knows the baseline, everybody has opinions about the UX, and everybody can spot where a framework makes the easy parts pleasant but the serious parts painful.
Linear, Notion and Jira are reminders that task software is not a solved toy category. A task may acquire an assignee, comments, due dates, workflow, dependencies, permissions, notifications, saved views, automation and years of history. Teams will move products for a better version of those interactions.
We are not using a to-do list because it is the smallest thing Nomos can build. We are using it because it can begin in five minutes and grow until it tests almost every property an ambitious application needs. The quality bar is simple to state: UX that can compete with the best task products, backed by DX an engineer can still understand from the domain file.
The application we are actually building
Section titled “The application we are actually building”Imagine a shared list called Launch the docs.
You create it and invite a collaborator as an editor. On the train, with no connection, you add “Write
the offline chapter” and tag it docs. At the same time your collaborator renames another item and marks
it complete from a different offline device. When both devices reconnect, the list converges without
either application having implemented a sync engine.
Open any item and you can follow its own story: who created it, which accepted offers changed it, which
law judged those offers and how the current state is obtained by replaying them. Later, version two of the
application replaces a simple done flag with a richer workflow. The old history is not rewritten or
thrown away; the evolution is explicit and replay remains deterministic.
That modest interface gives us somewhere concrete to demonstrate the whole system:
| What a person sees | What the tutorial proves |
|---|---|
| Lists and to-dos with stable links | Nomos mints durable identities; callers do not invent record IDs |
| An item timeline | Current state is a projection of accepted history, not the only copy of truth |
| Owner, editor and viewer sharing | Relationship-based authority is law, including list and individual-item grants |
| Useful work with the network switched off | The local participant remains capable; sync is not the write path |
| Both offline edits preserved or resolved predictably | Merge behaviour is declared per field and replayed by every kernel |
| A version-two app opening version-one history | Schema evolution is explicit, content-addressed and non-destructive |
| A restored app showing the same list | Custody can be discarded, restored, verified and replayed |
The law stays small
Section titled “The law stays small”The point is not to bury a tiny app under infrastructure. The core business model remains readable:
export const TodoList = aggregate("TodoList", { title: t.string().merge(Lww), owner: attributionField(t.string().merge(Lww)),}).public();
export const Todo = aggregate("Todo", { listId: t.ref(TodoList), text: t.string().merge(Lww), done: t.bool(), tags: t.set(t.string()).merge(AddWins),}).public();From those nouns we expose a small business language:
createListaddTodorenameTodotoggleTodotagTodoshareListshareTodo
The generated TypeScript and Dart applications speak that language. The UI does not grow its own repository, event model, permission cache or synchronization protocol alongside it.
The journey
Section titled “The journey”1. Make the familiar version
Section titled “1. Make the familiar version”First we build the version everyone expects: create a list, add an item, rename it, tag it and mark it done. Compilation generates the application client and the local proof from the same law.
This establishes the baseline: a usable application surface without hand-written persistence plumbing.
2. Give every item one identity and a real history
Section titled “2. Give every item one identity and a real history”createList and addTodo ask Nomos to mint identities. A client never chooses a convenient UUID and
hopes every other participant agrees.
An accepted change appends to the causal offer chain. The item detail screen can therefore show the offers that touched one to-do while the live explorer shows that item in the wider workspace lineage. Renaming an item changes its history and projection; it does not replace the business thing.
This is where a toy CRUD example becomes an auditable application.
3. Share by relationship, not by scattered conditionals
Section titled “3. Share by relationship, not by scattered conditionals”A list has an owner ⊃ editor ⊃ viewer role hierarchy. Its to-dos inherit those roles through their
parent relationship, while one individual to-do may also be shared directly with somebody who cannot see
the rest of the list.
export const TodoListRoles = roleHierarchy("TodoList", { owner: [], editor: ["owner"], viewer: ["editor"],});
export const TodoRoles = docSharingModel("Todo", "TodoList");The tutorial will prove both sides of this boundary: an editor’s offer is accepted, while the same offer from a viewer is refused with a typed result. The rule is checked at authoring, admission and replay—not only hidden behind a button in one UI.
4. Turn the network off
Section titled “4. Turn the network off”Two participants open the same shared list, disconnect and keep working. One edits text and the other adds
tags. Lww defines the text resolution; AddWins means concurrent tags form a union. Every participant
folds the same declared rules and reaches the same bytes after synchronization.
There is no “offline mode” copy of the application and no queue of database requests pretending to be local first. The local participant makes lawful offers immediately and synchronization exchanges custody later.
5. Evolve the schema after history exists
Section titled “5. Evolve the schema after history exists”Version one deliberately begins with done: boolean. Version two needs open, doing, done and
cancelled, plus optional priority and due date.
That is the moment most tutorials silently reset their database.
Nomos compares the candidate law with the committed identity of the previous law. Compatible additions can pass directly. A removal, contract change or merge-driver change stops at compile time and demands an explicit evolution. The tutorial will add that evolution, replay version-one offers under version two and prove that the same old history now produces the intended new shape.
Nothing edits historical commits in place. The evolution itself is inspectable law.
6. Destroy the cache and get the same world back
Section titled “6. Destroy the cache and get the same world back”Finally we discard the resident projection, restore the workspace from custody, verify its chain and replay it. The restored application must show the same lists, sharing relationships, item histories and open-item count.
This is the useful distinction between a server and authority: a server can store, transport and replay the application, but it cannot make an invalid offer true merely by returning a row.
Every chapter earns a proof
Section titled “Every chapter earns a proof”The tutorial is not complete when the screenshots look plausible.
| Chapter | Proof signal |
|---|---|
| Business language | The generated application exposes only the declared to-do verbs and typed reads |
| Item history | The same minted item reference survives every accepted change |
| Collaboration | Owner/editor offers are accepted and viewer writes are refused under the same law |
| Offline first | Two disconnected participants work, synchronize and converge byte-identically |
| Evolution | Version-two law replays version-one history without rewriting it |
| Recovery | A fresh kernel restores custody, verifies the chain and reads the same world |
The live lineage explorer then makes those proofs legible rather than leaving them buried in a test log.
Start with what is real today
Section titled “Start with what is real today”The published toolchain already provides the local compile-and-proof loop, generated TypeScript and Dart applications, custody restore and chain verification. You can prove that loop with no account:
npx --yes create-githolon@latest one-last-todocd one-last-todonpm installnpm run compilenpm run proofThe currently published scaffolder still emits the previous placeholder domain. It proves the toolchain, but it is no longer the teaching narrative. The complete to-do journey above is the contract for the new default starter and flagship tutorial. The domain and framework capabilities exist; the polished public UI, collaboration, evolution and explorer hand-offs will land as independently provable slices.
Continue with the runnable quickstart, learn how to model a domain, or open the live explorer.