Skip to content

Sync Backends ​

Cortexa supports encrypted cross-device sync via two backend types: filesystem (for synced folders like Dropbox or OneDrive) and S3-compatible object storage (AWS S3, Cloudflare R2, Backblaze B2, MinIO). All data is encrypted client-side before leaving the device -- sync backends only ever see ciphertext.

How Sync Works ​

Device A                    Remote Backend              Device B
┌──────────┐                ┌──────────┐                ┌──────────┐
│ cortexa.db│               │ manifest │ (encrypted)    │ cortexa.db│
│ blobs/    │──── push ────►│ blobs/   │ (encrypted)    │ blobs/    │
│ salt.bin  │               │ vault.json│ (plaintext)   │ salt.bin  │
└──────────┘                └──────────┘                └──────────┘
                                │                           │
                                └──────── pull ────────────►│

Each sync cycle:

  1. Snapshot local state -- build a manifest of all items with their updated_at timestamps.
  2. Fetch remote manifest -- download and decrypt the remote manifest.json.
  3. Pull -- for each item that is newer remotely (or remote-only), download the blob and apply it locally.
  4. Push -- for each item that is newer locally (or local-only), upload the blob.
  5. Merge -- create a union manifest using last-write-wins on updated_at and upload it.
  6. Upload vault metadata -- update vault.json with the KDF algorithm and salt hex so other devices can bootstrap.

Remote File Layout ​

KeyEncrypted?Contents
manifest.jsonYes (AES-256-GCM)JSON array of item entries with IDs, timestamps, summaries, deleted flags
vault.jsonNoKDF algorithm name + salt hex (no secrets)
blobs/<id>.binYes (already encrypted on disk)Item content blobs, uploaded as-is

Conflict Resolution ​

Cortexa uses last-write-wins (LWW) on the updated_at timestamp. During the merge step:

rust
fn merge(local: &[ManifestEntry], remote: &[ManifestEntry]) -> Vec<ManifestEntry> {
    // For each item ID, keep the entry with the later updated_at
    // Items that exist on only one side are included as-is
}
  • If both devices edited the same item, the one with the later updated_at wins.
  • If only one device has the item, it is included in the merged manifest.
  • Tombstoned items (deleted) participate in LWW -- a delete wins over an older edit.

The SyncReport tracks how many conflicts were resolved:

rust
pub struct SyncReport {
    pub uploaded: usize,
    pub downloaded: usize,
    pub conflicts_resolved: usize,
    pub last_synced_at: DateTime<Utc>,
}

CRDTs are a potential future refinement but LWW handles the vast majority of single-user multi-device scenarios correctly.

Filesystem Backend ​

Use any synced folder (Dropbox, OneDrive, Google Drive, a network share) as the remote storage.

Setup ​

  1. Open Settings > Sync in the desktop app.
  2. Set backend to Filesystem.
  3. Enter the folder path (e.g., C:\Users\you\Dropbox\cortexa-sync or /Users/you/Dropbox/cortexa-sync).
  4. Enable sync and save.
  5. Click Sync (or use the tray menu > Sync now).

How It Works ​

The filesystem backend implements the SyncBackend trait with simple file I/O:

  • get(key) -- reads a file at <path>/<key>.
  • put(key, bytes) -- writes a file at <path>/<key>.
  • delete(key) -- removes the file.
  • list() -- checks if manifest.json exists.

The folder structure mirrors the remote file layout:

cortexa-sync/
  manifest.json     # Encrypted manifest
  vault.json        # KDF metadata (plaintext)
  blobs/
    <id>.bin        # Encrypted item blobs

The cloud sync service (Dropbox, OneDrive, etc.) then syncs this folder to other devices.

S3-Compatible Backend ​

Tested with AWS S3, Cloudflare R2, Backblaze B2, and MinIO.

Setup ​

  1. Open Settings > Sync in the desktop app.
  2. Set backend to S3-compatible.
  3. Fill in the configuration fields:
FieldDescriptionExample
EndpointS3 API endpoint (blank for AWS default)https://xxx.r2.cloudflarestorage.com
RegionAWS region or auto for R2us-east-1, auto
BucketBucket namemy-cortexa-sync
PrefixOptional key prefix (share a bucket between vaults)vault-1/
Access KeyS3 access key IDAKIA...
Secret KeyS3 secret access keywJal...
Path-style URLsUse path-style instead of virtual-hostedOn for R2/MinIO/B2
  1. Enable sync and save.
  2. Click Sync to push.

Provider-Specific Notes ​

AWS S3 ​

  • Leave Endpoint blank (uses the default s3.<region>.amazonaws.com).
  • Set Region to your bucket's region (e.g., us-east-1).
  • Path-style can be off (virtual-hosted is the default).
  • IAM policy needs: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket.

Cloudflare R2 ​

  • Endpoint: https://<account-id>.r2.cloudflarestorage.com
  • Region: auto
  • Path-style: On
  • Create an R2 API token with Object Read & Write permissions.

Backblaze B2 ​

  • Endpoint: https://s3.<region>.backblazeb2.com
  • Region: The region shown in your B2 bucket settings (e.g., us-west-004).
  • Path-style: On
  • Use a B2 application key with read/write access to the bucket.

MinIO ​

  • Endpoint: http://your-minio-host:9000 (or HTTPS if configured).
  • Region: Can be anything (MinIO ignores it, but the field must not be empty).
  • Path-style: On
  • Create an access key pair in the MinIO console.

Auto-Sync ​

Set auto_sync_minutes to a non-zero value in settings to enable automatic periodic sync. The app runs a sync cycle every N minutes in the background.

Connect to Existing Vault ​

To set up a second device with an existing vault:

Flow ​

  1. On the new device, install Cortexa but do not capture anything yet.
  2. Open Settings > Sync and enter the same backend configuration as the first device.
  3. Enable sync and save.
  4. Expand Connect to an existing vault.
  5. Enter the passphrase from the first device.
  6. Click Connect.

What Happens ​

                          New Device
                              │
                              │ 1. Fetch vault.json from remote
                              v
                    ┌─────────────────┐
                    │  vault.json     │
                    │  version: 1     │
                    │  salt_hex: ...  │
                    │  kdf: argon2id  │
                    └────────┬────────┘
                             │
                             │ 2. Derive key from salt + entered passphrase
                             v
                    ┌─────────────────┐
                    │  Argon2id       │
                    │  (salt, phrase) │
                    │  → 256-bit key  │
                    └────────┬────────┘
                             │
                             │ 3. Try to decrypt remote manifest
                             v
                    ┌─────────────────┐
                    │  manifest.json  │ ← download + decrypt
                    │  (encrypted)    │
                    └────────┬────────┘
                             │
                     Success? ──── No ──→ Error: wrong passphrase
                             │
                            Yes
                             │
                             │ 4. Re-key the local vault to the
                             │    remote salt + passphrase
                             │ 5. Store passphrase in OS keychain
                             v
                    ┌─────────────────┐
                    │  Ready!         │
                    │  Next sync      │
                    │  pulls items.   │
                    └─────────────────┘

Key safety properties:

  • A wrong passphrase is detected before any local state is modified (the remote manifest decrypt fails).
  • A corrupted remote manifest is also detected and reported.
  • The salt is written to the local salt.bin only after successful verification, and only as part of the re-key: it is staged as salt.rekey.bin and moved into place with the re-encrypted blobs after the database commit, so a crash at any step leaves the vault wholly on the old key or wholly on the new one.
  • The passphrase is stored in the OS keychain.

After restart, the first sync pulls all items from the remote backend.

vault.json Manifest ​

The vault.json file is uploaded in plaintext alongside the encrypted manifest. It contains no secrets:

json
{
  "version": 1,
  "salt_hex": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "kdf": "argon2id"
}
FieldDescription
versionSchema version (currently 1)
salt_hexHex-encoded 16-byte salt (same as salt.bin)
kdfKey derivation function identifier

This file enables the "Connect to existing vault" flow on new devices. The salt alone is not secret -- it is useless without the passphrase.

SyncConfig Fields ​

The full sync configuration in settings.json:

json
{
  "sync": {
    "enabled": true,
    "backend": "s3",
    "filesystem_path": "",
    "s3_endpoint": "https://xxx.r2.cloudflarestorage.com",
    "s3_region": "auto",
    "s3_bucket": "my-cortexa-sync",
    "s3_prefix": "",
    "s3_access_key": "...",
    "s3_secret_key": "...",
    "s3_path_style": true,
    "auto_sync_minutes": 0
  }
}
FieldTypeDefaultDescription
enabledboolfalseMaster sync toggle
backendenum"filesystem""filesystem" or "s3"
filesystem_pathstring""Path to the sync folder
s3_endpointstring""S3 API endpoint (blank for AWS default)
s3_regionstring"us-east-1"AWS region or auto for R2
s3_bucketstring""Bucket name
s3_prefixstring""Optional key prefix
s3_access_keystring""Access key ID
s3_secret_keystring""Secret access key
s3_path_stylebooltrueUse path-style URLs
auto_sync_minutesu320Auto-sync interval (0 = disabled)

WARNING

S3 credentials are stored in settings.json in plaintext. The UI shows a warning about this. Migrating secrets to the OS keychain is a planned improvement.

Test Connection ​

The Test connection button in the Sync settings panel sends a lightweight probe to verify the backend is reachable:

  • Filesystem: Checks that the directory exists and is writable.
  • S3: Calls the list() method, which attempts to read manifest.json. Reports success or the exact error.

Always test the connection before your first sync to catch credential or network issues early.

Limitations ​

  • Audio sync: Briefing audio files are not yet included in the sync manifest. Briefings created on one device are not synced to others.
  • Single-user focus: LWW conflict resolution works well for one person across multiple devices. Multi-user editing of the same vault (two people syncing to the same bucket) may produce unexpected results.
  • No incremental manifest: The entire manifest is re-uploaded on every sync. For very large vaults (tens of thousands of items), this adds a small overhead. Incremental manifests are a potential optimization.

Built with Rust, Tauri, and a lot of captured thoughts.