Cross-Device Sync
Toto syncs across every device you use -- web dashboard, desktop agent, mobile client. Changes propagate in real-time when you're online and catch up automatically when you reconnect. Here's how it works.
The Model
Every mutation in Toto (creating a list, adding an item, changing status, deleting) increments a per-user sequence number. This sequence number is the sync clock. Your devices track the last sequence number they've seen and only fetch what's changed since then.
This means:
- No full refreshes. A device that synced 5 minutes ago fetches only the mutations from the last 5 minutes.
- No unnecessary data transfer. If you have 500 items but only changed 3, the sync payload contains 3 items.
- Deterministic ordering. Every mutation is ordered by sequence number, so devices always converge to the same state.
Delta Sync
The core sync operation is a single API call:
curl -s "https://toto.up.railway.app/api/sync/changes?since=2950" \
-H "Authorization: Bearer your_device_token"
This returns everything that changed since sequence 2950:
{
"current_seq": 2974,
"lists": [
{"id": "rDJpQekZ", "name": "Google OAuth", "sync_seq": 2960, ...}
],
"items": [
{"id": "px6K4KkE", "task": "Add OAuth middleware", "status": "done", "sync_seq": 2970, ...}
],
"tombstones": [
{"entity_type": "listitem", "entity_id": "abc123", "sync_seq": 2955, "deleted_at": "..."}
],
"resync_required": false
}
The client applies these changes to its local store, then saves current_seq (2974) for the next sync. On the next call, it passes since=2974 and gets only newer mutations.
Tombstones
When an item or list is deleted, the server creates a tombstone -- a record that says "this entity was deleted at this sequence number." Tombstones ensure that offline clients learn about deletions when they reconnect.
Without tombstones, a device that goes offline before a deletion would still have the deleted item in its local store. When it syncs, it would see no update for that item (because the item no longer exists on the server) and keep showing it. Tombstones solve this by explicitly communicating deletions through the same delta sync channel.
Resync
If a client's since value is higher than the server's current_seq (which can happen after a server migration or database reset), the response includes resync_required: true. The client should discard its local state and do a full sync with since=0.
Conflict Resolution
When two devices change the same item simultaneously, Toto uses Last-Writer-Wins (LWW) based on timestamps.
How It Works
- Device A changes an item at 2:00:00 PM
- Device B changes the same item at 2:00:03 PM
- Both push their changes via
POST /api/sync/push - The server compares timestamps: Device B's change wins because 2:00:03 > 2:00:00
- Device A's change is returned in the
conflictsarray of the push response
The push response tells each client exactly what happened:
{
"current_seq": 2975,
"resolved": [
{"entity_type": "listitem", "entity_id": "px6K4KkE", "fields": {"status": "done"}}
],
"conflicts": [],
"clock_skew_detected": false
}
Clock Skew Protection
Device clocks can drift. A phone 10 minutes fast would unfairly win every conflict. Toto clamps timestamps that are more than 5 minutes in the future to the server's current time. If clamping occurs, the response includes clock_skew_detected: true.
What This Means in Practice
LWW works well for personal use where you're the only one editing your tasks. The "conflict" scenario is typically you editing on your phone while a background sync from your laptop pushes an older state. The most recent edit wins, which is almost always what you want.
For team use cases, LWW means two people editing the same task text simultaneously will lose one person's edit. The status field (pending/done/in_progress) is less conflict-prone because transitions are typically sequential.
Real-Time Updates (SSE)
For the web dashboard, Toto provides real-time push via Server-Sent Events:
curl -N "https://toto.up.railway.app/api/events?detail=full" \
-H "Authorization: Bearer toto_your_token"
Events are user-scoped. You only receive events for your own data. The stream includes:
- Item status changes (triggers animations on the dashboard)
- New items created
- Items deleted
- List changes
Event Format
event: item_updated
data: {"item_id": "px6K4KkE", "list_id": "rDJpQekZ", "status": "done"}
With ?detail=full, the data payload includes the complete entity with all fields.
Keepalive
The server sends keepalive comments every 15 seconds to prevent connection timeouts:
: keepalive
Connection Management
Max 5 SSE connections per user. Opening a 6th connection evicts the oldest. The dashboard automatically reconnects if the SSE connection drops.
Offline-First Architecture
The desktop agent and planned mobile clients use a local-first architecture:
- Local SQLite store -- all data is persisted locally. The app works fully offline.
- Sync engine -- periodically fetches delta changes and pushes local mutations when a connection is available.
- Queue-and-replay -- mutations made offline are queued and replayed when connectivity returns.
The Sync Cycle
1. Fetch: GET /api/sync/changes?since={last_seq}
2. Apply server changes to local store
3. Push: POST /api/sync/push with local mutations
4. Resolve any conflicts
5. Save new current_seq
6. Repeat on interval (desktop: 30s polling)
The web dashboard uses SSE for real-time push instead of polling. The desktop agent uses polling with configurable intervals.
Supported Clients
| Client | Sync Method | Status |
|---|---|---|
| Web dashboard | SSE (real-time push) | Production |
| Desktop agent (macOS, pywebview + rumps tray) | Polling + sync engine | Beta — Swift native app in development |
| Claude Code (slash commands) | Direct API calls | Production |
| MCP server | Direct API calls | Production |
| iOS / macOS native (SwiftUI + WidgetKit) | Planned | Planned |
Building a Custom Client
To build a client that syncs with Toto:
- On startup, fetch all data:
GET /api/sync/changes?since=0 - Store
current_seqfrom the response - Periodically (or on demand) fetch:
GET /api/sync/changes?since={current_seq} - Apply changes: upsert lists/items, delete tombstoned entities
- To push changes:
POST /api/sync/pushwith your mutations - Handle conflicts from the push response
- For real-time: connect to
GET /api/eventsSSE stream
See API Reference for full endpoint documentation.
What Happens in Practice
The typical experience:
- You create a task list from Claude Code via
/toto-plan. The web dashboard updates instantly (SSE). - You mark a task in-progress via
/toto-start. The card starts glowing on the dashboard within a second. - You commit code. The post-commit hook runs reconciliation and marks the task done. The completion animation fires on the dashboard.
- Your desktop agent (if running) syncs on its next poll cycle (30 seconds). The macOS tray shows updated task counts.
- You open the dashboard on your phone. It fetches delta changes since your last visit and shows the current state.
All of this happens automatically. You never manually sync.
Further Reading
- API Reference — Sync API endpoints (delta fetch, push, SSE)
- Claude Code Integration — How slash commands interact with the API