10 KiB
SimpleSync Protocol Specification
Overview
SimpleSync is Cairnquire's intended synchronization model for asynchronous markdown collaboration. It covers three related surfaces:
- The Go server watches the configured content directory and reflects local filesystem edits into the document index.
- The Go-served browser UI caches documents in IndexedDB, queues offline edits, and resolves conflicts before writing over newer server content.
- Native or external clients can use snapshot/delta endpoints for bidirectional file sync.
The protocol is content-addressed: every document version is identified by the SHA-256 hash of its bytes. The server never accepts a write that claims an older base hash without returning a conflict response.
Design Goals
- Automatic: Local filesystem edits and browser edits propagate without a manual import/export step.
- Conflict-aware: Concurrent edits to the same file are detected and surfaced; they are not silently overwritten.
- Offline-capable: Browser edits can be queued locally and synced when connectivity returns.
- File-level simple: Different files can be edited independently without CRDTs, branches, or a git mental model.
- Verifiable: Content hashes are recomputed by the server on every stored version.
Milestone 2 Scope
Included:
- Recursive filesystem watching for markdown documents.
- Debounced sync after editor save bursts.
- Ignoring hidden files, hidden directories, and common editor temp files.
- WebSocket document-change notifications.
- IndexedDB document cache and pending edit queue.
- HTTP save conflict detection using
baseHash. - Manual browser conflict resolution with server/queued diff and resolved Monaco editor.
- Native snapshot/delta/content endpoints for future external clients.
- Simple fixed-size cleanup for cached rendered pages/content.
Out of scope for Milestone 2:
- WebSocket authentication. Authentication and authorization belong to Milestone 3 and should be integrated with the final app session model, not a temporary shared secret.
- Playwright or browser-level automated sync tests.
- Performance benchmarking for large sync sets.
- Quota-aware IndexedDB eviction. Milestone 2 uses a fixed entry cap instead.
Content Addressing
All persisted document content is immutable and addressed by SHA-256:
hash = sha256(document_bytes)
store path = <store-dir>/<hash[0:2]>/<hash[2:4]>/<hash>
Identical content deduplicates naturally. The latest document table points at the current hash for each path.
Filesystem Watcher
The server watches the configured source directory recursively.
Watcher behavior:
- Watch existing directories on startup.
- Add watches for newly-created directories.
- Process markdown paths only:
.mdand.MD. - Ignore hidden path segments such as
.git,.cache, and.note.md. - Ignore common editor temporary files such as
*.swp,*.tmp,*.bak,*.orig,*~, and#file#. - Debounce rapid event bursts before running a full
SyncSourceDir(). - Keep the 30 second polling fallback so missed fsnotify events are eventually reconciled.
The watcher callback performs a full source-directory sync. This keeps the fsnotify layer simple and avoids depending on platform-specific event details for correctness.
Browser Editor Sync
The browser editor uses the document's current server hash as an optimistic concurrency token.
Load
{
"path": "guide.md",
"content": "# Guide\n",
"hash": "server-hash"
}
The browser stores the loaded content and hash in IndexedDB.
Save
{
"content": "# Guide\n\nLocal edit\n",
"baseHash": "server-hash"
}
If baseHash still matches the server's current hash, the server writes the file, stores the new content hash, and returns the updated document.
If the server has changed since the browser loaded the document, the server returns 409 Conflict:
{
"status": "conflict",
"path": "guide.md",
"baseHash": "server-hash",
"currentHash": "new-server-hash",
"currentContent": "# Guide\n\nServer edit\n"
}
The browser keeps the queued local edit in IndexedDB, shows a diff between the server copy and the queued edit, and lets the user produce a resolved document. Submitting the resolved document uses the latest currentHash as the next baseHash.
Offline Queue
When a save cannot reach the server, the browser records a pending edit:
{
"path": "guide.md",
"content": "# Guide\n\nQueued edit\n",
"baseHash": "server-hash",
"status": "queued",
"updatedAt": "2026-05-14T12:00:00Z"
}
On reconnect or manual sync, queued edits are retried. Conflicts remain local until resolved by the user.
Cache Cleanup
The browser keeps pending edits until they sync or the user resolves them. Rendered page/content caches use a fixed entry cap and evict the oldest cachedAt records after successful cache writes. This gives predictable bounded growth without adding quota-estimation complexity before production hardening.
Realtime Notifications
WebSocket notifications are advisory. They tell connected browser clients that indexed server state changed; durable sync correctness still comes from hash-checked HTTP reads and writes.
Current intended message families:
{
"type": "document_version",
"path": "guide.md",
"hash": "new-server-hash"
}
{
"type": "folder_tree_changed"
}
Clients should reconnect automatically and refresh state after reconnect. If notifications are missed, the polling fallback and next document load still converge on server state.
Native Snapshot/Delta Sync
External clients use HTTP endpoints instead of browser IndexedDB state.
Initialize
POST /api/sync/init
{
"deviceId": "macbook-pro-1",
"rootPath": "/Users/alice/Documents/cairnquire"
}
Response:
{
"snapshotId": "snap:device:timestamp",
"serverSnapshot": [
{
"path": "guide.md",
"hash": "sha256...",
"size": 42,
"modified": "2026-05-14T12:00:00Z"
}
]
}
Delta
POST /api/sync/delta
{
"snapshotId": "snap:device:timestamp",
"clientDelta": [
{
"type": "update",
"path": "guide.md",
"hash": "client-hash",
"content": "# Guide\n\nClient edit\n",
"size": 58,
"modified": "2026-05-14T12:05:00Z"
}
]
}
Response:
{
"serverDelta": [
{
"type": "create",
"path": "new-note.md",
"hash": "server-hash",
"size": 100,
"modified": "2026-05-14T12:03:00Z"
}
],
"newSnapshotId": "snap:device:new-timestamp",
"conflicts": [
{
"path": "guide.md",
"serverHash": "server-hash",
"clientHash": "client-hash",
"serverModified": "2026-05-14T12:03:00Z",
"clientModified": "2026-05-14T12:05:00Z",
"strategy": "manual-merge"
}
]
}
For create/update deltas, native clients should send inline UTF-8 markdown in
content. The server stores the bytes content-addressably, verifies any claimed
hash, writes the markdown into the content tree, and returns a fresh
newSnapshotId for the client's next sync request.
Resolve
POST /api/sync/resolve
{
"snapshotId": "snap:device:timestamp",
"resolutions": [
{
"path": "guide.md",
"strategy": "server-wins"
}
]
}
Response:
{
"newSnapshotId": "snap:device:new-timestamp"
}
Content Fetch
GET /api/content/{hash}
Returns immutable raw bytes:
Content-Type: application/octet-stream
Cache-Control: public, max-age=31536000, immutable
ETag: "<hash>"
Conflict Rules
A same-file conflict exists when:
- The client is editing from a known base hash.
- The server's current hash for that path changed after that base hash.
- The client attempts to write different content for the same path.
Different paths do not conflict. Deletions and renames are represented in the native delta model and should be resolved at file-path granularity.
Resolution strategies:
manual-merge: User edits a resolved document and saves it against the latest server hash.server-wins: Keep the current server version.client-wins: Replace the server version with the client version after explicit user choice.rename-both: Preserve both versions using a conflict filename.last-write-wins: Allowed for automation, but not the default browser UX because it can hide data loss.
State Machine
online
+----------------------------------+
| v
+-----+-----+ edit/save ok +-----+------+
| Reading +--------------------->+ Synced |
+-----+-----+ +-----+------+
| ^
| edit while offline |
v |
+-----+------+ reconnect/manual sync |
| Queued +---------------------------+
+-----+------+
|
| server hash changed
v
+-----+------+
| Conflicted |
+-----+------+
|
| user resolves against latest hash
v
+-----+------+
| Synced |
+------------+
Error Codes
| Code | Description | Retryable |
|---|---|---|
conflict |
Write base hash is stale | No, user resolution required |
not_found |
Requested document or content hash is unknown | Sometimes |
hash_mismatch |
Content does not match claimed hash | Yes, after recompute |
too_large |
Content exceeds configured size limit | No |
rate_limited |
Too many requests | Yes, with backoff |
server_error |
Internal server error | Yes |
Security Considerations
- Milestone 2 does not add temporary WebSocket auth.
- Milestone 3 should apply session/authz checks consistently to document reads, document writes, WebSocket upgrade, and sync endpoints.
- The server must canonicalize paths and reject traversal outside the configured content root.
- The server recomputes SHA-256 before storing or accepting content.
- Raw HTML markdown support is intended for trusted content repositories; untrusted public authoring requires a sanitizer policy before launch.
Version
Protocol Version: 1.0
Last Updated: 2026-05-14