Skip to main content

Storage Layout

Server​

All server-side data lives under a single path prefix (the library root). Data blobs are stored only in encrypted form.

/
├── remote_id_{uuid} UUID of this remote (empty marker file, one per remote root)
├── library/
│ ├── library_id_{uuid} UUID of this library (encoded in filename)
│ ├── library_salt random 32-byte salt
│ ├── version_1 write format version
│ └── mk_{username}_{uuid}.enc master key encrypted with the user's KEK
├── media/
│ └── YYYY/
│ └── MM/
│ ├── {UUID}.data encrypted photo/video bytes
│ └── {UUID}.thumb encrypted thumbnail
└── operations/
├── {UUID}.op{N}_{count} encrypted compacted CRDT operations (tier N, count operations)
└── LOCK.op compaction lock (JSON, transient)

The remote_id_{uuid} marker sits at the root of each remote's storage, alongside library/, media/, and operations/ when the remote holds a full copy. It is written once by initialize_remote and checked on every subsequent connect, so a client can detect a stale or mismatched remote before syncing against it.

On-device​

All application data is stored under a single root directory (the app dir), which defaults to the platform's application data directory:

Operating SystemDefault Path
Linux~/.local/share/lasco
macOS~/Library/Application Support/lasco
Windows%LOCALAPPDATA%\lasco

The root can be overridden via the --data-dir CLI flag.

{app dir}/
├── config.json global index of libraries
└── libraries/
└── {library_id}/
├── library.json per-library config and remotes
├── local_state/
│ ├── library/
│ │ ├── library_id_{uuid} UUID of this library (encoded in filename)
│ │ ├── library_salt random 32-byte salt
│ │ ├── version_1 write format version
│ │ └── mk_{username}_{uuid}.enc master key encrypted with the user's KEK
│ ├── crdt-state.enc encrypted CrdtState and Lamport clock
│ ├── operations.log append-only encrypted frames of individual CRDT operations
│ └── media/
│ └── YYYY/
│ └── MM/
│ ├── {UUID}.data
│ └── {UUID}.thumb
└── remotes/
└── {remote_id}/
└── state/ last known state of the remote
├── compact_op_id_merged_to_local.json `CompactedOpId` values for operation files already merged locally
├── operations/ downloaded remote op files
└── media/
└── media_list.json last known remote media availability

Each library directory is self-describing. Its configuration, including library_nickname, lives in library.json next to the library data, so the whole directory can be copied or moved as a unit. The global config.json is only a thin index used to discover libraries and select the default one.

remotes/{remote_id}/state/compact_op_id_merged_to_local.json is a local Fetch-progress marker. It stores the UUIDs (CompactedOpId) of immutable operations/{uuid}.op{tier}_{count} files whose operations have already been applied to CrdtState and recorded in the local operation log. It does not contain individual operation dots, operation contents, or remote file ciphertext.

config.json​

The global index. It records library IDs and the default library ID. It holds no names, remote, or credential data.

{
"default_library_id": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"libraries": [
"a1b2c3d4-5678-90ef-ghij-klmnopqrstuv"
]
}

library.json​

Per-library configuration stored at {app dir}/libraries/{library_id}/library.json. It holds the library nickname, preferences, and ordered list of remotes. Each remote entry carries a stable remote_uuid, a human-readable name, and a kind object with that kind's connection details, including, for credentialed remotes, the encrypted secret key.

An empty remotes list is valid: the library is local-only and has no remote storage layout. The library/, media/, and operations/ server layout is created only after a configured remote is initialized.

{
"library_nickname": "my-photos",
"device_id": "0123456789abcdef0123456789abcdef",
"default_username": "alice",
"active_password_uuid": "c3d4e5f6-7890-12gh-ijkl-mnopqrstuvwx",
"default_fetch_remote": "a1b2c3d4-0000-0000-0000-000000000001",
"auto_import_device_media": false,
"remotes": [
{
"remote_uuid": "a1b2c3d4-0000-0000-0000-000000000001",
"name": "my-s3-remote",
"media_fetch_priority": 0,
"exclude_from_media_fetch": false,
"kind": {
"kind": "s3",
"endpoint": "https://s3.amazonaws.com",
"bucket": "my-photos",
"region": "us-east-1",
"access_key": "AKIAIOSFODNN7EXAMPLE",
"secret_key_encrypted": "<base64-encoded AES-256-GCM ciphertext>",
"secret_key_encryption_description": "AES-256-GCM v1"
}
},
{
"remote_uuid": "a1b2c3d4-0000-0000-0000-000000000002",
"name": "usb-backup",
"media_fetch_priority": 1,
"exclude_from_media_fetch": false,
"kind": {
"kind": "fixed_path",
"root_dir": "/Volumes/backup/my-photos"
}
}
]
}

remote_uuid is the remote's stable identity. It is checked against the remote_id_{uuid} marker on the remote's storage on every connect, and never changes even if the human-readable name is later renamed. name is how the CLI and app let a user refer to the remote. It must be unique within a library but can be changed freely without affecting sync.

Media blobs have no independent deletion record in storage. Their lifecycle is defined by encrypted CRDT operations: a trashed item's .data and .thumb objects remain, while a hard-deleted item's objects are reclaimed only after its MediaDeletion operation is pushed to that particular remote. Consequently, a remote can temporarily retain unreachable encrypted blobs until it is next pushed; the tombstone, not the presence of an object at a media path, is authoritative.

device_id is the stable, local-only CRDT author identity for this installation of the library. It is never uploaded to a remote. The encrypted crdt-state.enc is a rebuildable materialized cache and deliberately does not persist this identity; a recovery replays operations.log using library.json.device_id.

The kind field's own kind tag selects the remote's storage type:

  • s3 — an S3-compatible bucket, reached with the given credentials.
  • fixed_path — trusts the stored root_dir absolute path as-is. Suitable for USB drives, network mounts, or any path stable across the app's lifetime.
  • debug_local_apple (macOS/iOS only) — stores only a local_dir_name and re-resolves its location against the current app-support directory on every use, because sandboxed containers get a fresh container path on each install or reinstall. This is a development and testing convenience, not for production backup use.

media_fetch_priority controls on-demand media retrieval. A lower value is tried first; equal values retain the configuration order. Set exclude_from_media_fetch to true for a remote that must not be queried for on-demand media. When media is absent locally, clients try each eligible remote in priority order until one supplies it.

The secret key is never stored in plaintext. It is encrypted with the library master key using AES-256-GCM and held only in the secret_key_encrypted field. Local filesystem remotes have no credentials. See Encryption for details.