6 Rust crates, Automerge CRDTs, Ed25519 cryptographic identities, and libp2p networking. Boards merge automatically. Nothing ever phones home.
Architecture
6-crate Rust workspace. Core CRDT logic is UI-agnostic — shared by CLI and Tauri desktop. Sync is purely peer-to-peer via libp2p.
Workspace
Each crate owns a single layer. Core CRDT logic is independent of UI — the CLI and desktop app share identical behavior.
Domain Model
Automerge CRDT document logic. Owns board/column/card/checklist/chat data structures, all mutations, and migrations. init_doc() creates the root Automerge structure. Every mutation is a CRDT operation — concurrent edits from multiple peers merge automatically.
Cryptography
Ed25519 identity management using ed25519-dalek. Identity::generate() creates a fresh keypair from OsRng. Node IDs are base32-encoded public keys with a pk_ prefix. Sign() and verify() underpin invite tokens, space membership, and all P2P messages.
P2P Networking
libp2p swarm with mDNS peer discovery on the local network and optional bootstrap peers for WAN. QUIC transport. Custom sync protocol exchanges Automerge sync messages. AnnounceSpaces and SyncBoard commands flow via mpsc channels to the background tokio task.
Persistence
SQLite via rusqlite (bundled). Stores board rows with serialized Automerge binary blobs. Space and membership tables hold invite state. Card number index (card_id → number) is maintained for fast CLI lookups. Schema managed by schema.rs migrations.
CLI
Clap-based command tree covering every feature: board create/list/delete, column add/move, card add/move/assign/label/due, checklist, comment, space create/invite/join/kick, sync start, and ai-help (structured JSON schema output for AI agents).
Desktop App
Tauri v2 desktop app (drag-and-drop kanban UI). Calls the same core crates via Rust backend. Supports column sorting, card covers, labels, due dates, assignees, checklists, and QR code invite generation. No separate UI binary — one executable for both CLI and GUI.
Data Flow
From key generation to peer sync — every step, from identity creation to CRDT convergence.
First run: monotask-crypto generates an Ed25519 keypair from OsRng. Public key is base32-encoded as the node ID (pk_abc123…). Private key is stored encrypted in ~/.monotask/identity. Alternatively, import from an existing SSH Ed25519 key.
Identity::generate() → pk_<base32(pubkey)>monotask board create 'Sprint 5' creates an Automerge AutoCommit document. init_doc() writes the root structure: columns (List), cards (Map), members (Map), actor_card_seq (Map), label_definitions (Map). The binary document blob is stored in SQLite alongside a board row.
AutoCommit::new() → init_doc() → storage.save_board(board_id, doc.save())card::add_card() appends to the columns List and cards Map as Automerge transactions. Each card gets a UUIDv7 ID and an actor-scoped card number (actor_card_seq). Concurrent edits from different peers — add card, move card, edit title — merge without conflict via Automerge's CRDT semantics.
doc.put_object(&cards_map, card_id, ObjType::Map) → doc.commit()monotask space create 'Team Alpha' registers a Space with an Ed25519-signed metadata block. monotask space invite generates a time-limited token: JSON payload (space_id, pubkey, expiry, permissions) serialized via CBOR, signed with the creator's signing key, base58-encoded. Recipient decodes, verifies signature, and joins.
InviteMetadata → ciborium::to_vec() → sign() → bs58::encode()When sync is enabled, monotask-net starts a libp2p swarm on UDP/TCP port 7272. mDNS broadcasts discover peers on the local network automatically. For WAN, bootstrap_peers can be pre-configured. On peer connect: AnnounceSpaces command tells the peer which Space IDs this node participates in.
Swarm::new() → mDNS behaviour → PeerConnected → AnnounceSpacesOn board sync: both peers run the Automerge sync protocol — exchange sync messages (binary diffs) until convergent. Peer A sends SyncMessage; Peer B applies generate_sync_message → receive_sync_message loop. When sync is complete, the merged document is saved back to SQLite. Both peers now have identical board state.
doc.generate_sync_message() ↔ doc.receive_sync_message() → storage.save_board()Every command supports --json for structured output: boards list returns [{id, name, columns_count, cards_count}]. ai-help prints a full JSON schema of all commands and their arguments — AI agents (Claude, Cursor) can read this to learn the API without documentation.
monotask board list --json | jq '.[] | .name'CRDT
Every board is one Automerge AutoCommit document. Concurrent mutations from any peer merge deterministically — no conflict resolution logic required.
Document Root Structure
Automerge AutoCommit with 5 top-level keys: columns (List), cards (Map), members (Map), actor_card_seq (Map), label_definitions (Map)
Card ID Format
UUIDv7 (time-ordered) ensures consistent sort order across peers without coordination
Card Number Format
actor_card_seq[actor_id]++ — each actor maintains its own counter, preventing collisions: ALICE-1, BOB-1 can coexist
Merge Semantics
Concurrent column reorders → Last-Write-Wins on List. Concurrent card edits → field-level merge. Concurrent deletes + edits → delete wins (is_deleted flag)
Sync Protocol
Automerge's built-in sync protocol: exchange sync messages in a loop until both sides agree. No custom merge logic required.
Storage Format
doc.save() returns the full Automerge binary. Stored as BLOB in SQLite boards.doc column. On load: AutoCommit::load(&blob)
Root Document Structure
ROOT (AutoCommit) ├── columns: List // ordered column IDs ├── cards: Map // card_id → Card object │ └── <uuid-v7>: Map │ ├── title, description, number │ ├── column_id, position │ ├── labels, assignees, due_date │ ├── is_deleted: bool │ └── checklist: List ├── members: Map // pubkey_hex → role ├── actor_card_seq: Map // actor_id → u64 counter └── label_definitions: Map
Cryptography
No accounts. No servers. Identity is an Ed25519 keypair. Invite tokens are signed payloads — verifiable offline by any peer.
Key Properties
Invite Token Flow
// Creator
let meta = InviteMetadata {
space_id, pubkey,
expiry, permissions,
};
let bytes = ciborium::to_vec(&meta);
let sig = identity.sign(&sha256(bytes));
let token = bs58::encode([bytes, sig]);
// Recipient
let (meta, sig) = bs58::decode(token);
Identity::verify(meta.pubkey, &meta, &sig)?;
// ✓ token is authenticPerformance
Rust + bundled SQLite ensures consistent performance across all platforms.
| Operation | Speed | Notes |
|---|---|---|
| Board load (100 cards) | <20ms | Automerge blob deserialize + SQLite read |
| Card add (CRDT commit) | <5ms | Single AutoCommit transaction |
| Invite token generate | <2ms | CBOR + Ed25519 sign |
| Invite token verify | <1ms | Ed25519 verify only |
| mDNS peer discovery | <3s | First discovery on LAN (multicast) |
| Automerge sync (1K ops) | <100ms | Binary sync message exchange |
| SQLite write (board save) | <10ms | Single BLOB write, WAL mode |
| CLI JSON output | <50ms | Includes SQLite read + serialization |