Context
v1 had to ship a trustworthy sync core on two operating systems with a small team. Every decision below trades scope for shippability and debuggability. The goal was one thing done bulletproof, not ten things done okay. These calls are locked; reopening any of them requires genuinely new information, not just a new mood.
The decisions
Language: Rust, not Go
(1) Swift FFI. macOS File Provider extensions are Swift binaries. Rust compiles to
cdylib and links directly. Go's runtime forces a separate process and an extra IPC layer at the exact boundary where we need the most reliability.(2) Windows CFAPI bindings.
windows-rs is Microsoft-official and idiomatic. Go's COM story is manual and poorly maintained.(3) State machine correctness under concurrency. The sync engine is a state machine where concurrent writes must be caught at compile time. Rust's
Send/Sync and borrow checker do this. This matters more when code is AI-assisted and may not be deeply reviewed.
Topology: Cloud-authoritative, not P2P
Cloud-authoritative gives one source of truth, deterministic conflict ordering (server
seq), and simpler ops. LAN peer-assist can be added later as an optimization on top of a correct cloud-ordered log — not as a topology change.
Build order: Engine-first, OS adapters later
Engine-first with the
PresentationAdapter interface means ~90% of client code is reusable when adapters arrive. The ~10% throwaway is the folder-watcher loop — the price of de-risking the engine first. The folder-watcher adapter still exercises the real engine, server protocol, SQLite state, conflicts, tombstones, and log replay.
Process architecture: Thin extension + daemon, not engine-in-extension
SQLite ownership, server credentials, WebSocket poll loops, retry policy, panic-resync state, and event emission need one durable local owner — shared by the CLI, Tauri app, and native provider. That owner is the per-user daemon (
tinysync-daemon). The extension is a thin callback translator; the daemon is the engine.
Desktop app role: Tauri as control plane only
If Tauri owned sync, quitting the app would stop sync. Files would only appear in a Tauri-managed window, not in Finder or Explorer. The native cloud-drive experience would be impossible. Tauri is the control plane. The daemon is the engine. The OS provider is the filesystem face.
IPC protocol: Bounded JSON over local sockets, not gRPC
JSON is not used for file bytes — hydration and local edits move by daemon-approved file references, so IPC stays bounded and readable. The cap of 1 MiB/frame prevents accidental large-payload abuse while keeping the wire format debuggable with
jq. The protocol is capability-negotiated at connect time so components can be updated independently without silent schema drift.
Write path: Native-provider writes bypass PresentationAdapter
PresentationAdapter models the eager/watcher case: the engine observes local filesystem state through an adapter, derives changes, and queues mutations. Native providers invert that flow. When a user saves a file in Finder, the OS extension is called with a concrete user operation — the daemon doesn't need to discover it by watching the filesystem.Forcing this through the adapter would add an artificial "watch and rediscover" layer for a change the OS already reported. The reuse boundary is enforced differently: daemon writes must share blob upload, mutation submission, idempotency, conflict detection, and accepted-event application helpers with the CLI/eager path. They must not invent provider-only mutation semantics.
Cloud-drive location, not arbitrary folders
~/Library/CloudStorage/<App>/. Apple removed the choice; Dropbox fought this and lost. Matching this layout on Windows keeps the product consistent across platforms and removes a class of user-configuration errors entirely.
Conflict policy: Optimistic concurrency with conflict-copy preservation
base_item_version. The first writer whose base version still matches the server is accepted as the canonical item. Later stale writers are rejected and their local content is preserved as conflict copies. This is not true last-writer-wins: a confirmed server write is not silently overwritten by a later stale write.Anything smarter — app-specific merge, manual conflict UI — can replace the conflict policy later without changing the storage model. The key property is that correctness is observable and auditable from the server log.
Auth: Capability tokens, not user accounts
The token model can attach to a parent user system that federates tokens. Keeping "user semantics" outside TinySync keeps the engine clean and agnostic to future shapes such as service accounts and agent access.
Superseded by Device Identity & Cardinality (2026-06): join tokens were removed; access is group membership.
Item identity: Item IDs, not paths
seq is the ordering truth for all operations; wall-clock timestamps are display-only.
Unified ClientEvent stream
Consequences
Taken together, these decisions make correctness, auditability, and a simple server straightforward to achieve: every mutation is cloud-ordered by seq, every conflict is preserved not silently overwritten, every device is independently revocable, and the engine's state is fully reproducible from the server log. The tradeoffs deferred to later are bandwidth efficiency (whole-file uploads instead of deltas), end-to-end encryption (server-side dedup and conflict diagnostics require plaintext access today), and offline-first collaboration (cloud-authoritative topology is the right foundation but requires additional work for concurrent multi-user edits on disconnected devices).