← Back to Sortify
Architecture

How Sortify Syncs: A Privacy-First Distributed Synchronization Architecture

An inside look at how Sortify keeps shared spaces in sync across devices, users, and cloud providers — without ever letting a server read your data.


The Problem We Set Out to Solve

Imagine two people share a workshop. One of them moves the label maker to a shelf on the left side. The other, not knowing, moves it to a drawer on the right. Now neither person knows exactly where it is — and Sortify has received two conflicting updates from two different devices at roughly the same time.

That scenario — two people, one shared space, two honest edits, one conflict — is the hardest problem in synchronization. And it gets harder when you add the requirements Sortify is built around:

Most sync systems solve these problems by relying on a central server: all clients send their changes to the server, the server merges everything, clients receive the authoritative result. That approach is well-understood and works well — but it requires the server to read and process your data. For Sortify, that was a non-starter.

Instead, we built something different: a synchronization engine that runs entirely on the client, uses cloud storage purely as a versioned file store, and guarantees that two devices cannot silently overwrite each other's changes. This article explains how it works, what failed during development, and what we ultimately built.


What Non-Technical Readers Need to Know

If you use Sortify and want to understand what sync actually does — without the distributed systems terminology — here is the plain version.

Your workspace data lives on your phone. That is the primary copy. When you add an item, move something, or rename a room, the change is saved to your device immediately, even if you have no internet connection.

When sync is turned on, an encrypted backup goes to your cloud account. This is not a live connection. It is more like taking a photograph of your entire workspace — encrypting it so only you can read it — and saving it to your Google Drive or OneDrive. The photo is updated automatically every few minutes, or whenever a significant change happens.

When two people share a workspace, their phones coordinate through this shared backup. When you make a change, your phone updates the photo. When your colleague makes a change, their phone updates the photo too. Before either phone uploads a new photo, it first checks whether the other person's phone has already updated it — and if so, it downloads the latest version, combines the changes intelligently, and then saves the combined result.

The critical guarantee is this: neither phone can accidentally erase the other person's changes. If two phones try to update the workspace at exactly the same moment, one of them wins, and the other one detects this, downloads the winner's version, combines it with its own changes, and tries again. No change is lost.

Nothing you store in Sortify ever passes through our servers in readable form. MokingBird never has access to the contents of your workspace.


The Architecture: Snapshot Sync With Client-Side Merge

Sortify uses an approach called snapshot-based synchronization with client-side merge and optimistic concurrency control. Here is what each of those terms means and why we chose this approach.

Snapshots, Not Delta Streams

Many sync systems send only what changed — a stream of individual edits ("item X was moved at 10:05", "room Y was renamed at 10:07"). This requires a server that can receive and apply those deltas in order, manage a shared edit history, and coordinate between clients. It is efficient in bandwidth but requires server-side computation and a server that can read the data.

Sortify sends the entire workspace as a single encrypted file. When you sync, your phone exports the full workspace database, encrypts it with a key that never leaves your device, and uploads the encrypted blob to your cloud account. When another device syncs, it downloads this blob, decrypts it, and merges it with its own local state.

The advantage: the cloud provider sees only an opaque encrypted file. No server reads or processes your data. The disadvantage: uploading the whole workspace is more bandwidth than uploading only what changed. For the kind of small-to-medium workspaces Sortify targets — a home, a lab, a workshop — this tradeoff is acceptable, and the privacy guarantee is non-negotiable.

Client-Side Merge

When two devices have diverged — each has changes the other does not know about — one of them must reconcile the differences. In Sortify, this happens entirely on the client device. After downloading the remote snapshot, the app compares it against the local database, applies a defined set of merge rules, re-applies any local changes that have not yet been published, and produces a merged result that combines both.

The cloud provider is treated as a versioned file store — a place to put and retrieve files — not a system with any intelligence about workspace contents.

Optimistic Concurrency Control

This is the mechanism that prevents two devices from silently overwriting each other. Before uploading a new snapshot, the app asks: "Is the remote file still the version I based my changes on?" If it is, the upload proceeds. If it is not — because another device uploaded a newer version in the meantime — the upload is rejected, and the app re-downloads, re-merges, and retries.

Each major cloud provider exposes this capability through version tokens:

Sortify stores the version token of the last successfully published snapshot. Every upload carries this token. If the token does not match what is currently on the provider — because someone else uploaded first — the upload fails safely, no data is overwritten, and the retry loop begins.


What Failed First

The synchronization architecture Sortify uses today was not the first design we tried. The first version had a fatal flaw that cost users their data under specific circumstances.

The "Absence Means Delete" Bug

The original pull logic worked like this: download the remote snapshot, compare it against local data, and delete any local rows that were not present in the remote snapshot. The reasoning was: if it is not in the remote, it must have been deleted by another user.

That reasoning is wrong in eventually consistent systems.

Suppose you add several items while offline. Those items exist only on your device. When you reconnect, the sync engine starts by downloading the remote snapshot — which, naturally, does not contain the items you just added locally because you have not uploaded yet. The engine then runs its comparison: "these items are local-only, not in the remote snapshot." And deletes them.

Items vanished. Rooms disappeared. Counts dropped to zero. The data was perfectly intact — right up until sync ran.

This revealed a critical principle in distributed system design:

Absence is not deletion. A missing entry in a remote snapshot tells you nothing about whether something was intentionally deleted or simply never uploaded yet.

Three Compounding Problems

The data-loss bug was actually the intersection of three separate failures:

1. Incomplete journaling. When you created or moved an item, the change was written to the local database — but not always recorded in the change_journal, the list of local changes that have not yet been synchronized. Without a journal entry, the sync engine had no way to know that a local item was new and not yet uploaded. It looked indistinguishable from a stale row that should be cleaned up.

2. Destructive pull-merge logic. The pull engine treated the remote snapshot as authoritative by default. Local rows that were missing from the remote snapshot were removed, with only a narrow exception for items that happened to be in the journal.

3. Pull before publish. In some sync cycles, the pull step ran before the push step. Local changes that had not yet been uploaded were already vulnerable to the destructive merge before they had any chance to be protected by a successful upload.

The fix for each of these was architectural, not cosmetic.


The Fix: Eight Phases of Implementation

The rebuild of the sync engine was done in eight distinct implementation phases, each addressing a specific failure mode.

Phase 1 — Non-Destructive Pull Merge

The pull logic was rewritten to never delete a local row based on its absence from the remote snapshot. Instead:

Deletion is now expressed exclusively through tombstones: is_active = 0 with a deleted_at timestamp. An item is only considered deleted if it carries an explicit deletion record — not because it is absent.

Phase 2 — Durable Sync State

A workspace_sync_state table was introduced. This persists, per workspace:

Before this, sync state was implicit — the app had to re-discover its position on every sync cycle. Now the position is explicit and durable. Debugging sync failures became dramatically easier; the sync engine always knows exactly what state it was in.

Phase 3 — Optimistic Concurrency on All Providers

The upload path for Google Drive, Dropbox, and OneDrive was updated to carry version preconditions. Before uploading, the engine reads the current remote version token. The upload is submitted with that token as a condition. If the remote file has changed since the token was captured — because another device published in the meantime — the upload fails, and the engine begins the pull-merge-push retry loop.

This converted the system from blind overwrite to safe optimistic concurrency.

Phase 4 — Bounded Retry Loop

Optimistic concurrency naturally produces retries. Two devices may sync at nearly the same moment; one succeeds and one gets a precondition failure. The failing device must pull again, merge again, and retry.

The retry loop is bounded — a maximum number of attempts with appropriate backoff — to prevent infinite retry cycles that drain battery or create sync storms under high-concurrency conditions.

Phase 5 — Complete Journal Coverage

Every mutation to workspace data — item creates, updates, moves, room renames, membership changes, tombstone deletes — now writes atomically to both the local database table and the change_journal. The journal entry and the data write happen in the same database transaction, so they cannot diverge. Either both succeed or neither does.

The journal is the list of local intent not yet safely published remotely. Without complete coverage, any mutation that was not journaled was invisible to the sync engine and potentially vulnerable to merge-time deletion.

Phase 6 — Deterministic Conflict Tie-Breaking

Two devices editing the same item at exactly the same timestamp is rare but possible. Without a deterministic rule for this case, two different devices might resolve the conflict differently, causing divergence that never converges.

The tie-breaker is lexical comparison of the device identifier or updated_by field. Equal timestamps always produce the same outcome on any device. This property — convergence regardless of which device runs the merge — is essential for eventual consistency.

Phase 7 — iCloud Gated as Unsupported for Collaboration

iCloud Drive does not expose the same optimistic concurrency primitives as Google Drive, Dropbox, and OneDrive. There is no clean public API for conditional uploads with version token enforcement in the way those three providers support it. The CloudKit database layer does support record versioning, but adopting it would require a fundamentally different architecture for iCloud — structured record sync rather than encrypted blob sync — and would break the provider-neutral design.

Rather than simulate correctness that does not exist, iCloud collaborative sync is explicitly gated as unsupported in the current release. The UI blocks workspace creation and invitation flows on iCloud until a proper implementation is built.

This was an engineering honesty decision: better to tell users a feature is not available than to ship a feature that silently loses data.

Phase 8 — Tombstone Delete Routing

Deletes now route through the tombstone system consistently. is_active = 0 is set with a deleted_at timestamp. The tombstone is included in the exported snapshot and propagates to other devices on next sync. The merge engine on the receiving end recognizes the tombstone and applies the deletion correctly, rather than receiving a missing row and guessing at the intent.


Google Drive: Targeted Workspace Authorization

Sortify's current Google Drive approach avoids broad Drive browsing for normal collaboration. Core workspace data is stored in an encrypted workspace.sortifypkg package, and optional workspace photos are stored separately in an encrypted photo_workspace.sortifypkg package.

Google Drive's drive.file model means a collaborator may not automatically have app-level access to a package created by another user's Sortify app, even if the file or folder is shared in Google Drive. To solve that without treating the user's whole Drive as a sync database, Sortify uses targeted Picker authorization. When a collaborator joins or enables Google Drive photo sync, the app asks Google to authorize the exact Sortify package file needed for that workspace.

This has two practical effects:

A Google Drive "file not found" response can occur before Picker authorization because Google has not yet granted the app access to that exact package. Sortify treats that as an authorization step, not as evidence that the user's data was deleted.


The Encryption Model

Every workspace snapshot is encrypted before it leaves the device.

Sortify uses AES-256-GCM encryption with a 96-bit random IV generated per-upload. The encryption key is workspace-specific and never transmitted to any server. On Android, the key is stored in the Android Keystore. On iOS, it is stored in the iOS Keychain. The cloud provider receives and stores an opaque binary blob — it cannot read, index, or process the workspace contents.

The metadata file that accompanies each snapshot — which contains workspace configuration, version information, and member lists — is also encrypted before upload. Neither the snapshot nor the metadata is readable without the workspace encryption key.


What a Sync Cycle Looks Like Today

Here is the full sequence of a sync cycle in the current implementation:

1. Read remote metadata. The sync engine fetches the metadata file from the provider and reads the current version token (etag, rev, or revision ID). This is a lightweight request — it does not download the full encrypted snapshot.

2. Check whether the remote changed. Compare the current remote version token against the last token stored in workspace_sync_state. If they match, the remote has not changed since the last successful sync. The pull step can be skipped.

3. Download and decrypt (if remote changed). If the remote has a newer version, download the encrypted snapshot, decrypt it using the local workspace key, and load it into a temporary database for merge processing.

4. Non-destructive merge. Apply remote-only rows locally. Apply Last-Write-Wins for rows that exist in both. Preserve tombstone deletions. Leave local-only rows untouched.

5. Replay unsynced journal entries. Re-apply all local changes recorded in the change_journal with synced = false. This ensures local intent survives the merge even if the remote had conflicting state.

6. Export and encrypt the merged result. Serialize the merged local database, encrypt it with the workspace key and a fresh random IV, and prepare it for upload.

7. Conditional upload. Submit the upload with the version token from step 1 as a precondition. The provider accepts the upload only if the remote file is still at that version. If another device has uploaded in the meantime, the upload is rejected.

8. On precondition failure, retry. If the upload is rejected, go back to step 1. The retry is bounded with backoff.

9. On success, persist the new version token. Store the new remote version token in workspace_sync_state. Mark journal entries as synced.

This produces eventual consistency across all members of a workspace without requiring a centralized merge server.


Cloud Providers: What Each Supports

ProviderVersion TokenConditional UploadCollaborative Sync
Google DriveRevision metadata + etagIf-Match headerSupported
Dropboxrev identifiermode: update with revSupported
OneDriveetag / cTagIf-Match via Microsoft GraphSupported
iCloud DriveWeak or unavailableNot cleanly supportedNot supported

iCloud Drive's absence from supported collaborative providers is not a data privacy concern — Apple's security is excellent. It is a concurrency primitives concern: the sync architecture requires strong optimistic concurrency control semantics, and iCloud Drive does not expose them cleanly for a backendless file-based sync model.


Why This Architecture Over the Alternatives

The design choice most worth defending is the snapshot-based approach. Delta sync — sending only what changed — is more bandwidth-efficient and enables finer-grained conflict resolution. But it requires server-side state: something that can receive and apply individual change records, maintain a change history, assign version numbers, and serve "give me all changes since version X" requests.

Sortify has no such server. Your cloud provider is a file storage system, not a database. Google Drive cannot answer "give me all item updates since last Tuesday." It can only give you the current file and tell you its version number.

Given that constraint, snapshot sync is not a compromise — it is the correct choice. The client-side merge engine provides the intelligence that a server would otherwise provide, and the encrypted snapshot provides the same privacy guarantee regardless of which provider the user has chosen.

The remaining cost is bandwidth: uploading the full workspace on every sync is heavier than delta sync. For the small-to-medium collaborative workspaces Sortify is designed for, this cost is acceptable. For very large workspaces, future releases may explore compression or incremental export optimization.


Known Tradeoffs and Future Work

Snapshot size. Full-workspace uploads are heavier than delta sync. For typical Sortify workspaces, this is acceptable. Very large workspaces, or workspaces with many high-resolution photos, may eventually benefit from chunked or incremental export.

Clock dependency. Last-Write-Wins conflict resolution depends on device timestamps being reasonably accurate. Devices with significantly incorrect clocks may produce unexpected merge outcomes. Logical clocks or hybrid logical clocks would eliminate this dependency but add complexity. At current scale, timestamp-based LWW is a practical tradeoff.

Retry storms under heavy concurrency. Workspaces with many simultaneous active users could produce repeated retry cycles if multiple devices are pushing changes continuously. Future improvements may include randomized retry jitter, change batching, and sync coalescing to reduce retry pressure.

Optional conflict review UI. The current release resolves all conflicts automatically using Last-Write-Wins. All change history — including overwritten values — is preserved in the change_journal for auditability. A user-facing conflict review interface may be introduced in a future release for high-value entities where the automatic resolution is insufficient.


The Core Insight

Distributed synchronization without a central authority is hard. Doing it while preserving end-to-end encryption, working offline, and keeping the user fully in control of their data storage is harder still.

The deepest architectural lesson from building Sortify's sync engine is this:

Synchronization correctness is less about "syncing data" and more about preserving user intent safely under concurrency.

When items disappeared from early versions of Sortify, the problem was not that the sync engine was slow or unreliable. The problem was that the engine did not know the difference between "this item does not exist yet on the remote" and "this item was deleted from the remote." It destroyed intent — things the user had deliberately created — because it could not distinguish new from deleted.

Every major architectural change since then — tombstones, complete journaling, optimistic concurrency, deterministic merge, non-destructive pull — was a direct answer to that single insight. The goal is not merely to exchange data between devices. The goal is to make sure that what you intended to happen in your shared space is what happens, on every device, regardless of connectivity, regardless of timing, and regardless of whether your teammate was syncing at the same moment.


Closing

Sortify's sync engine is unusual in its space. Most apps in the "track shared things" category either rely on centralized servers to manage all data, or they offer sync that is single-user only. Sortify attempts something harder: genuine multi-user collaborative sync, fully offline-capable, with end-to-end encryption, on infrastructure that belongs entirely to the user.

The architecture described here — snapshot sync with optimistic concurrency, client-side merge, tombstone convergence, and deterministic conflict resolution — is how we do it. It is not perfect. Snapshot sync costs more bandwidth than delta sync, and the current implementation does not yet include a user-facing conflict review UI or advanced retry coalescing. But it is correct: it does not lose your data, it does not overwrite your colleague's changes, and it does not require you to trust a server with your inventory.

For a tool built on the premise that your space's memory belongs to you — not to us — that is the architecture it deserved.


Sortify — Shared Memory for Your Space. MokingBird Oy — sortify.mokingbird.xyz Sortify is a registered brand of MokingBird Oy, Finland.


2026 Architecture Update: Sortify Cloud, Enterprise, And Photos

Sortify now supports two sync families.

The first is the user-owned cloud model described above: Google Drive, OneDrive, and Dropbox store encrypted workspace packages, and the app performs merge logic locally on each device.

The second is the managed Sortify model: Sortify Cloud and Enterprise SyncNative. In this model, the app still keeps SQLite as the primary local database and still encrypts content before upload. The difference is transport. Instead of moving one whole provider package on every collaboration change, the app sends encrypted events and encrypted checkpoints through Sortify-managed infrastructure. Other members receive those encrypted updates and apply them locally.

The privacy boundary remains the same: Sortify servers do not receive plaintext inventory content. Managed sync stores encrypted payloads plus operational metadata such as workspace IDs, membership state, timestamps, and sync position.

Enterprise SyncNative uses the same zero-knowledge transport foundation as Sortify Cloud, but it adds organization-level administration, enterprise workspaces, member controls, recovery/security surfaces, and business billing controls.

Photo sync is separate from core item sync. Workspace photos are optional per workspace and per device. If photo sync is off, rooms and items still sync. If it is on, Sortify uploads and downloads compressed encrypted reference photos. Google Drive uses targeted Picker authorization for exact workspace/photo package files, while OneDrive, Dropbox, Sortify Cloud, and Enterprise use their own encrypted package or managed-media paths.