Rust · Automerge · libp2p

How MonoTask
Syncs Without Servers

6 Rust crates, Automerge CRDTs, Ed25519 cryptographic identities, and libp2p networking. Boards merge automatically. Nothing ever phones home.

0Servers Required
Ed25519Identity Crypto
CRDTSync Protocol
6Rust Crates
libp2pP2P Stack
7272Default Port

Architecture

System Overview

6-crate Rust workspace. Core CRDT logic is UI-agnostic — shared by CLI and Tauri desktop. Sync is purely peer-to-peer via libp2p.

CLImonotask-cliDesktop Appmonotask-taurimonotask-coreAutomerge CRDT · board · card · column · spaceinit_doc() · mutations · migrationsmonotask-storageSQLite · Automerge blobs~/.monotask/data.dbmonotask-cryptoEd25519 · invite tokenssign · verify · bs58NETWORK LAYERmonotask-netlibp2p swarmmDNSdiscoveryQUICtransportRemote Peermonotask-netAutomerge syncport 7272(same Space)sync msgssave/load~/.monotask/data.dbboards · spaces · card_numbers~/.monotask/identityEd25519 signing key (encrypted)

Workspace

6 Rust Crates

Each crate owns a single layer. Core CRDT logic is independent of UI — the CLI and desktop app share identical behavior.

🃏

Domain Model

monotask-core

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.

automergeboard.rscard.rscolumn.rschat.rsspace.rs
🔐

Cryptography

monotask-crypto

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.

ed25519-dalekOsRngpk_ prefixinvite tokensSSH import
🌐

P2P Networking

monotask-net

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.

libp2pmDNSQUICport 7272tokiompsc
🗄

Persistence

monotask-storage

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.

rusqlitebundled SQLiteAutomerge blobsspace.rscard_number.rs
⌨️

CLI

monotask-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).

clapboardcardcolumnspaceai-help--json
🖥

Desktop App

monotask-tauri

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.

Tauri v2drag-and-dropQR inviteslabelsdue datescovers

Data Flow

End-to-End System Flow

From key generation to peer sync — every step, from identity creation to CRDT convergence.

1

Identity Creation

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)>
2

Board Creation

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())
3

Card Operations (CRDT Mutations)

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()
4

Space Creation & Invite

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()
5

P2P Discovery (mDNS)

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 → AnnounceSpaces
6

Automerge Sync Exchange

On 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()
7

CLI Output (--json)

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

Automerge Document Model

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

Ed25519 Identity System

No accounts. No servers. Identity is an Ed25519 keypair. Invite tokens are signed payloads — verifiable offline by any peer.

Key Properties

Key AlgorithmEd25519 (ed25519-dalek v2)
Key SourceOsRng (OS random, not user entropy)
Node ID Formatpk_ + base32(pubkey, RFC4648 no-padding)
Invite PayloadCBOR-encoded InviteMetadata (ciborium)
Invite Encodingbs58 (Base58Check) — human-copyable
SignatureEd25519 over CBOR payload SHA-256 hash
SSH Importssh-key crate — parse OpenSSH Ed25519 privkey
Key Storage~/.monotask/identity (caller-encrypted)

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 authentic

Performance

Speed Benchmarks

Rust + bundled SQLite ensures consistent performance across all platforms.

OperationSpeedNotes
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