Mosaic📔

Sync

How multi-device synchronization works in Mosaic

Sync

Mosaic uses a pull-based synchronization protocol that lets the mobile app stay in sync with the server.

Overview

The sync system is designed for offline-first mobile use:

  • The mobile app stores data locally in SQLite
  • Changes are synced with the server on demand
  • Conflicts are resolved with "last-writer-wins"
  • Each entity type tracks its own sync state independently

Protocol

The sync uses a timestamp-based cursor approach.

Pull Request

The client sends its current cursors (timestamps) for each entity type:

POST /api/sync/pull
{
  "clientId": "unique-device-id",
  "cursors": {
    "memo": 1715000000000,
    "diary": 1715000000000,
    "resource": 1715000000000,
    "bot": 1715000000000
  }
}

Pull Response

The server returns changes since each cursor:

{
  "cursors": {
    "memo": 1715080000000,
    "diary": 1715080000000,
    "resource": 1715080000000,
    "bot": 1715080000000
  },
  "changes": {
    "memo": {
      "updated": [ { "id": "...", "content": "...", ... } ],
      "deletedIds": [ "deleted-memo-uuid" ]
    },
    "diary": { "updated": [...], "deletedIds": [...] },
    "resource": { "updated": [...], "deletedIds": [...] },
    "bot": { "updated": [...], "deletedIds": [...] }
  }
}

Push

The client can push local changes to the server via standard CRUD API endpoints before pulling.

Entity Types

EntityDescriptionDeletion
MemoNotes with content, tags, and AI summarySoft delete (is_deleted)
DiaryDaily mood summary entriesSoft delete (is_deleted)
ResourceFile attachments (images, videos)Soft delete (is_deleted)
BotAI bot configurationsSoft delete (is_deleted)

Conflict Resolution

Mosaic uses last-writer-wins based on the updated_at timestamp (milliseconds). The most recent change always takes precedence.

Batch Size

Each pull queries up to 200 changed rows per entity, including deletion markers. The current Go implementation advances response cursors to server time, without a truncation flag. An overfull batch can therefore skip records on subsequent pulls. Check completeness during large initial syncs; a reliable paginated backfill needs further server work.

Sync Cursors

Sync state is stored in the sync_cursors table:

CREATE TABLE sync_cursors (
    client_id VARCHAR(64) NOT NULL,
    user_id UUID NOT NULL,
    entity_type VARCHAR(32) NOT NULL,
    last_sync_at BIGINT NOT NULL,
    PRIMARY KEY (client_id, user_id, entity_type)
);

Each device gets its own cursor state, so multiple devices can sync independently.

On this page