Skip to main content

Motivations & Design Decisions

No single point of failure

The primary goal of Lasco is to store photos without a single point of failure. That means storing data across multiple locations and not depending on a single provider. To achieve this, all application logic lives in the client, which can connect to multiple storage backends (cloud providers, local disks, external hard drives) using the same primitives.

This also reduces the risk of things going wrong: no server to deploy or maintain means fewer mistakes compared to self-hosting a server application. It also limits exposure to compromised dependencies or new vulnerabilities. However, this simplicity comes at a cost: without a server to coordinate writes, the system must handle conflicts on its own.

Shared access model

To simplify implementation and encryption management, all files are considered accessible by all users of a library.

Git-inspired remote model

Inspired by Git, Lasco introduces the concepts of a remote, push, and fetch:

  • A remote is any storage backend (a cloud drive, a local disk, an external hard drive).
  • Push uploads local state to a remote.
  • Fetch downloads remote state to the local device.

This model covers two important use cases:

Replication. There is no dedicated "backup" operation. Instead, replication is achieved by pushing to multiple remotes. Adding a photo and pushing to two storage backends is all it takes to have two copies. The push/fetch abstraction is the only primitive needed.

Sporadic backups. Remotes do not need to be permanently connected. An external hard drive can serve as a backup target: connect it, push, and disconnect. To restore from it later, connect it again and fetch. This works naturally within the same model.

Two remarks

Push and fetch are not symmetric operations. Fetch never downloads media files. It only retrieves what is needed to reconstruct the library state: administrative files and operation files. Each photo and thumbnail is downloaded individually, on demand, as its own explicit operation.

Since media are rarely present locally, a push often involves downloading from one remote and uploading to another. Lasco therefore tracks which media each remote already contains.

Conflict-free design

Because no server coordinates writes, concurrent modifications from multiple clients could produce conflicts. Lasco adopts a CRDT-inspired operation log: every local change is recorded as an operation appended to a log. When pushing or fetching, operation logs are merged. This merge is always well-defined because two invariants are enforced for the vast majority of stored files:

  1. Unique random names. Every new file is given a randomly generated name, making name collisions effectively impossible.
  2. Immutability. Once created, a file is never modified in place. The one exception is the local pending-operations file, which accumulates operations before they are uploaded.

Always-representable state

Any combination of valid operations must yield a state the client can present to the user. If concurrent operations produce an inconsistency (e.g. a cycle in the album tree), the affected objects are shown in a dedicated UI section rather than hidden or automatically fixed. The user resolves it by emitting further operations.

CRDT operation ordering

To reconstruct the library state, operation groups are sorted into a total order and applied sequentially. The order is derived from a causal DAG: each group carries a parent_op_id pointing to the group the author had last seen when they wrote it. A topological sort of this DAG ensures a group is applied only after its parent. When multiple groups share the same parent (concurrent writes from different clients), they are ordered by op_id ascending. Operation IDs are UUIDv7, which embed a millisecond-precision timestamp, so this tie-break gives "earliest clock wins" on concurrent forks while remaining fully deterministic.