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:
- Snapshot local state -- build a manifest of all items with their
updated_attimestamps. - Fetch remote manifest -- download and decrypt the remote
manifest.json. - Pull -- for each item that is newer remotely (or remote-only), download the blob and apply it locally.
- Push -- for each item that is newer locally (or local-only), upload the blob.
- Merge -- create a union manifest using last-write-wins on
updated_atand upload it. - Upload vault metadata -- update
vault.jsonwith the KDF algorithm and salt hex so other devices can bootstrap.
Remote File Layout
| Key | Encrypted? | Contents |
|---|---|---|
manifest.json | Yes (AES-256-GCM) | JSON array of item entries with IDs, timestamps, summaries, deleted flags |
vault.json | No | KDF algorithm name + salt hex (no secrets) |
blobs/<id>.bin | Yes (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:
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_atwins. - 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:
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
- Open Settings > Sync in the desktop app.
- Set backend to Filesystem.
- Enter the folder path (e.g.,
C:\Users\you\Dropbox\cortexa-syncor/Users/you/Dropbox/cortexa-sync). - Enable sync and save.
- 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 ifmanifest.jsonexists.
The folder structure mirrors the remote file layout:
cortexa-sync/
manifest.json # Encrypted manifest
vault.json # KDF metadata (plaintext)
blobs/
<id>.bin # Encrypted item blobsThe 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
- Open Settings > Sync in the desktop app.
- Set backend to S3-compatible.
- Fill in the configuration fields:
| Field | Description | Example |
|---|---|---|
| Endpoint | S3 API endpoint (blank for AWS default) | https://xxx.r2.cloudflarestorage.com |
| Region | AWS region or auto for R2 | us-east-1, auto |
| Bucket | Bucket name | my-cortexa-sync |
| Prefix | Optional key prefix (share a bucket between vaults) | vault-1/ |
| Access Key | S3 access key ID | AKIA... |
| Secret Key | S3 secret access key | wJal... |
| Path-style URLs | Use path-style instead of virtual-hosted | On for R2/MinIO/B2 |
- Enable sync and save.
- 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
- On the new device, install Cortexa but do not capture anything yet.
- Open Settings > Sync and enter the same backend configuration as the first device.
- Enable sync and save.
- Expand Connect to an existing vault.
- Enter the passphrase from the first device.
- 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.binonly after successful verification, and only as part of the re-key: it is staged assalt.rekey.binand 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:
{
"version": 1,
"salt_hex": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"kdf": "argon2id"
}| Field | Description |
|---|---|
version | Schema version (currently 1) |
salt_hex | Hex-encoded 16-byte salt (same as salt.bin) |
kdf | Key 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:
{
"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
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Master sync toggle |
backend | enum | "filesystem" | "filesystem" or "s3" |
filesystem_path | string | "" | Path to the sync folder |
s3_endpoint | string | "" | S3 API endpoint (blank for AWS default) |
s3_region | string | "us-east-1" | AWS region or auto for R2 |
s3_bucket | string | "" | Bucket name |
s3_prefix | string | "" | Optional key prefix |
s3_access_key | string | "" | Access key ID |
s3_secret_key | string | "" | Secret access key |
s3_path_style | bool | true | Use path-style URLs |
auto_sync_minutes | u32 | 0 | Auto-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 readmanifest.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.