Durable local state and recovery come first. A synchronization layer only makes sense once a desktop app can survive a crash on its own.
Local-first is a product promise
Tract is a private-beta desktop voice-to-text product with an on-device, privacy-conscious workflow. That verified boundary is the context for this article. The synchronization and collaboration patterns below are general engineering considerations for a possible future networked layer; they are not descriptions of features currently available in Tract.
That promise needs a boundary. Not every feature can function without a network, and not every dataset belongs on every device. Define which reading, creation, editing, search, and export operations work locally; which require a server; how long credentials remain useful; and what happens when a device is lost. “Offline supported” is too vague to design or test.
Model local state as durable, inspectable data
A robust client needs more than current records. It may need stable entity identifiers, local operation identifiers, actor and device identity, schema version, logical ordering, pending changes, synchronization checkpoints, deletion markers, and conflict state. Persist enough intent to retry safely after a crash without guessing whether a change reached the server.
- Generate collision-resistant identifiers without waiting for a server round trip.
- Make local operations idempotent so replay after interruption does not duplicate effects.
- Treat deletion as information that must synchronize, not an immediate absence that peers cannot explain.
- Version the local schema and test upgrades from realistically old application versions.
- Protect sensitive local data with platform-appropriate encryption, credential handling, and remote revocation assumptions.
Synchronization is a protocol, not a background fetch
Sync must answer what changed, since when, according to whom, and in which order. Wall-clock timestamps alone are unreliable across devices and reconnects. A server-issued cursor, append-only operation sequence, entity version, or another monotonic mechanism can make progress and replay explicit. Compaction may be necessary, but it should preserve the information required for convergence and audit.
- Record local intent
Commit the user’s operation and its identifier locally before presenting it as safely saved.
- Exchange from checkpoints
Push pending operations and pull authoritative changes from a resumable cursor.
- Apply deterministic rules
Validate permissions and versions, deduplicate operations, and merge only where the domain has a defensible merge function.
- Advance after durable apply
Move the checkpoint only after changes are committed, so interruption can replay without creating gaps.
- Surface unresolved state
Keep conflicts and rejected operations available for a person or domain-specific recovery flow.
Conflicts are product decisions
Last-write-wins is simple, but it can silently discard a thoughtful edit because another device’s clock or reconnect happened later. Automatic merging works for some sets, counters, or independent fields, but can violate invariants for schedules, approvals, or ordered content. Choose conflict semantics by domain object and operation, not one global rule.
When human resolution is required, show the meaningful difference and its origin. Asking a user to choose between two opaque JSON versions transfers architecture complexity to them. Preserve both inputs, explain which fields or actions conflict, and offer a safe preview. In collaborative products, presence and early warnings can prevent conflicts, but they should not be required for correctness.
Make synchronization trustworthy and test hostile timelines
Users need simple language for local, pending, synchronized, conflicted, and rejected state. A green cloud icon is not enough if it does not distinguish “queued on this device” from “accepted by the server.” Provide last successful sync, relevant pending work, a retry path, and export or support recovery for serious failures. Avoid blocking normal local work because unrelated synchronization is slow.
Test with reordered and duplicated messages, long offline periods, expired credentials, schema upgrades, low storage, interrupted transactions, deleted accounts, revoked access, and several devices editing the same records. Property-based tests can check convergence and idempotency; scenario tests can validate product behavior. For any future networked layer, local-first quality would depend on designing time and uncertainty explicitly, not on treating the network as a request that occasionally fails.