Storage
Lix runs in memory by default. Choose a storage adapter when you need to keep data across restarts. The same SQL works on every storage adapter.
Opening and upgrades
open_lix() in Rust and openLix() in JavaScript own repository opening, including supported format upgrades. Use the same call for a new, current, or supported older repository. Upgrades retain source data and validate the candidate before activating it. Unsupported formats and replica states that require recovery fail without replacing the repository with an empty one.
let lix = lix::open_lix()
.with_storage(storage)
.on_progress(|event| eprintln!("{:?}: {:?}", event.scope, event.phase))
.await?;
let upgrades = &lix.open_report().migrations;
const lix = await openLix({
storage,
onProgress(event) {
console.log(event.scope, event.phase);
},
});
const upgrades = lix.openReport.migrations;
Progress identifies local or authority work and can report inspecting, migrating, validating, opening, and completion. The open remains pending during supported upgrades. Progress callbacks are observers: a UI exception cannot stop an upgrade. The immutable opening report records observed upgrades by scope and source/target format; an unchanged repository has no local migration entry.
With a server configured, opening waits for explicit authority-upgrade responses. Existing replicas can still reopen from their durable state when the network is unavailable; authentication and identity failures are not treated as offline success. Browser sharing additionally requires its existing verified credential proof. Omitting the server opens an existing partial replica offline without making a network request.
Commit acknowledgement
Repositories on durable storage wait for the storage backend's durable boundary before acknowledging writes by default. This applies to automatic statements, batches, explicit transaction commits, and additional sessions opened from the handle. RocksDB synchronizes its WAL; SlateDB waits for the WAL upload; OPFS uses SQLite synchronous=FULL. Memory remains ephemeral even with the default policy.
Applications that accept losing acknowledged saves after a crash can opt into buffered acknowledgement when opening local storage:
import { openLix } from "@lix-js/sdk";
import { FilesystemStorage } from "@lix-js/storage-filesystem";
const lix = await openLix({
storage: new FilesystemStorage({ path: "./repository" }),
durability: "buffered", // default: "durable"
});
In Rust, use open_lix().with_storage(storage).with_durability(Durability::Buffered). The policy belongs to the open repository handle, not the stored repository format. Reopening without the option restores the durable default. It does not weaken internal durability requirements for publication, migration, or sync. Buffered acknowledgement differs by adapter: RocksDB can lose writes on power loss; SlateDB can also lose writes when its process exits before WAL upload.
Durability covers repository storage, not completion of background filesystem exports or synchronization with a server. For remote execution the authority chooses its storage policy; remote clients cannot override it. Hardware and storage services must honor their synchronization guarantees. Tests verify the backend requests and acknowledgement boundaries, not physical power failures. Durable acknowledgement can increase write latency. Use a batch or explicit transaction to group related changes into one commit. The commit_durability example measures a small sequential RocksDB workload under both policies.
In-memory (default)
Start with openLix() and no options. Data lives in memory for the lifetime of the instance, making this useful for tests and trying Lix:
import { openLix } from "@lix-js/sdk";
const lix = await openLix();
// ... use it ...
await lix.close();
Local filesystem
Use @lix-js/storage-filesystem in Node.js to store a repository in a directory. Agents and tools can read and write its ordinary files:
import { openLix } from "@lix-js/sdk";
import { FilesystemStorage } from "@lix-js/storage-filesystem";
const lix = await openLix({
storage: new FilesystemStorage({ path: "./repository" }),
});
Lix stores repository state in <repository>/.lix/.internal. Keep that state with the directory to reopen it. Only regular files synchronize; symlinks and special entries are excluded.
For selective sync, pass syncAllFiles: false and import paths with storage.importPaths(paths). See Rust usage below.
Filesystem sync
A partial replica with on-demand sync runs locally alongside the authoritative server. Use FilesystemStorage for project directories or mounted sandbox volumes. It keeps ordinary files synchronized with the replica, which exchanges commits with the server in the background.
import { openLix } from "@lix-js/sdk";
import { FilesystemStorage } from "@lix-js/storage-filesystem";
const lix = await openLix({
storage: new FilesystemStorage({ path: "/workspace/project" }),
server: {
mode: "partial_replica",
url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
},
});
Point path at the directory your infrastructure mounts into the sandbox. Each machine keeps its own replica. Share a branch to exchange changes, or use separate branches for review. See opening and reconnecting for on-demand loading and offline behavior.
Remote mode
A classic client-server setup: your app sends requests through the Lix SDK, and the server executes them against its repository. Pass server without storage. Storage is managed on the server. Use LixRay or your own host, and replace the example URL with your Lix connection URL.
import { openLix } from "@lix-js/sdk";
const lix = await openLix({
server: {
url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
},
});
Each operation requires a network round trip; successful writes are accepted by the server. For ordinary files on disk, use filesystem sync.
Browser OPFS
OpfsStorage stores the repository in the browser across reloads. Add server: { url: repositoryUrl, mode: "partial_replica" } alongside storage to create a partial replica with on-demand sync.
Opening loads bounded metadata. SQL fetches missing native inputs and retains them locally. Reads and writes whose dependencies are resident run locally, including offline; local commits upload in the background.
Background synchronization adopts remote branch updates atomically without first fetching everything that previous queries read. A query or observer evaluating the new state fetches its own missing inputs before returning a coherent result. A previously cached query can therefore need network data after a remote update; missing data is never returned as an empty result. Applications can mount their workspace when opening completes and show loading separately for each pending query.
import { openLix } from "@lix-js/sdk";
import { OpfsStorage } from "@lix-js/storage-opfs";
const lix = await openLix({
storage: new OpfsStorage({ name: "acme" }),
server: {
mode: "partial_replica",
url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
},
});
Install @lix-js/storage-opfs. SQLite Wasm stores the replica in the browser's Origin Private File System (OPFS). Reuse name within the same browser origin to reopen it after reloads. Omit server for a browser-only repository. Workers and tabs can share the same name through the package's storage worker and cross-tab Web Lock.
These configurations create a partial replica with on-demand sync. Opening loads bounded metadata; SQL fetches missing native inputs and caches them locally. Reads and writes whose dependencies are resident execute locally, including offline. Local commits upload in the background. server.mode defaults to "remote", which rejects storage; the partial-replica opt-in is required. See opening and reconnecting.
How storage adapters fit
The client configures its local adapter with storage. The host configures server storage. In the diagram, SlateDB runs inside the server process and uses S3 as its external backing store.
| Adapter | Available in | Stores data in |
|---|---|---|
Memory (default) | JavaScript, Rust | Temporary in-memory data |
FilesystemStorage | JavaScript (Node.js), Rust | Files and repository state on disk |
OpfsStorage | JavaScript (browser) | Browser OPFS through SQLite Wasm |
RocksDB | Rust | Local disk for native embedded storage |
SlateDB | Rust | S3-compatible object storage |
The reference server uses SlateDB; custom hosts can choose another adapter. The separate server option controls remote execution or replica synchronization. See the connection reference. JavaScript partial replicas require a durable adapter, such as OpfsStorage or FilesystemStorage. Use Snapshots to export or restore a complete repository.
Rust filesystem adapter
In Rust, start directory synchronization explicitly:
use lix::open_lix;
use lix_storage_filesystem::FilesystemStorage;
let storage = FilesystemStorage::new("./repository").open()?;
let lix = open_lix().with_storage(storage.clone()).await?;
storage.start_sync(&lix).await?;
storage.sync_disk_to_lix().await?;
storage.stop_sync().await?;
The adapter owns directory synchronization after start_sync(). Stop it before immediately reopening the directory; dropping the final instance attempts shutdown.
Automatic format upgrades
Opening a supported older format copies it into an inactive storage epoch, validates it, then atomically publishes it. No separate migration call is needed. Report progress with Rust's OpenProgressSink or JavaScript's onProgress.
Lix retains the previous generation for rollback. Budget roughly 2× the live repository size plus WAL, compaction, and temporary-write space. Upgrade time depends on data size, storage, and hardware; available capacity is the practical limit. Later upgrades reuse the inactive epoch and reclaim legacy storage asynchronously.
Closing
Always await lix.close() in scripts and tests. Long-lived servers can hold one Lix instance for the process lifetime.
Custom storage (Rust)
Adapters implement ordered transactional key-value storage, without parsing Lix SQL or interpreting branches and changes.
Implement three asynchronous traits from lix::storage: Storage, StorageRead, and StorageWrite. An implementation must guarantee:
- Space isolation. Keys in different spaces never collide.
- Coherent read views. A read handle observes one coherent view for its lifetime.
- Ordered scans. Scans return keys in ascending byte order.
- Atomic commits. A commit publishes all staged mutations or none.
- Durability. Durable implementations define their durability boundary.
Memoryis ephemeral.
Validate an implementation with the public conformance suite:
use lix::storage::conformance::run_storage_conformance;
let report = run_storage_conformance(&factory).await;
report.assert_no_failures();
Backends without an existing adapter, such as PostgreSQL or Cloudflare D1, need such an implementation.