Getting Started

Lix is a JavaScript SDK for change control that enables Git-like capabilities for apps and agents: Change proposals, versioning (branching), history, blame, and more.

  • 🌐 Works in browsers and Node.js environments
  • 🔌 Works with any data format (Excel, JSON, CSV, etc.)
  • 🤖 Made for Human to Human and Human to AI agent collaboration

Lix features

Installation

Install the Lix SDK package:

npm
yarn
pnpm
bun
npm install @lix-js/sdk @lix-js/plugin-json

Opening a Lix

Open a lix with openLix() and provide plugins for files types you are going to use.

import { openLix  } from "@lix-js/sdk";
import { plugin as jsonPlugin } from "@lix-js/plugin-json";

const lix = await openLix({
  providePlugins: [jsonPlugin],
});

Key points:

  • The jsonPlugin enables Lix to understand JSON file changes at a granular level (e.g., detecting which specific properties changed)
  • Without plugins, Lix would only see files as binary blobs

Insert a File

Inserting a file is the first step to start controlling changes with Lix. When you insert a file:

  1. Lix controls the changes in the file - From this point forward, every change of the file is tracked and versioned
  2. Any file type is supported - While this example uses JSON, Lix can handle any file format (CSV, Excel, images, etc.). See a list of available plugins in the Plugins section.
import { openLix  } from "@lix-js/sdk";
import { plugin as jsonPlugin } from "@lix-js/plugin-json";

const lix = await openLix({
  providePlugins: [jsonPlugin],
});
const json = {
  name: "Peter",
  age: 50,
};

await lix.db
  .insertInto("file")
  .values({
    path: "/example.json",
    data: new TextEncoder().encode(JSON.stringify(json)),
  })
  .execute();

const fileAfterInsert = await lix.db
  .selectFrom("file")
  .select(["path", "data"])
  .executeTakeFirstOrThrow();

console.log("File after insert:", [
  {
    ...fileAfterInsert,
    data: JSON.parse(new TextDecoder().decode(fileAfterInsert.data)),
  },
]);

Key points:

  • Files are stored as Uint8Array (binary data), making Lix format-agnostic
  • The path acts as the unique identifier for the file
  • Once inserted, the file becomes part of Lix's change control system

Updating Files

Once a file is under Lix's change control, updates work just like regular database operations. No special commands or workflows - just update the data and Lix handles the rest.

import { openLix  } from "@lix-js/sdk";
import { plugin as jsonPlugin } from "@lix-js/plugin-json";

const lix = await openLix({
  providePlugins: [jsonPlugin],
});

const json = {
  name: "Peter",
  age: 50,
};

await lix.db
  .insertInto("file")
  .values({
    path: "/example.json",
    data: new TextEncoder().encode(JSON.stringify(json)),
  })
  .execute();

const fileAfterInsert = await lix.db
  .selectFrom("file")
  .select(["path", "data"])
  .executeTakeFirstOrThrow();

console.log("File after insert:", [
  {
    ...fileAfterInsert,
    data: JSON.parse(new TextDecoder().decode(fileAfterInsert.data)),
  },
]);
// we update the user's age to 51
await lix.db
  .updateTable("file")
  .where("path", "=", "/example.json")
  .set({
    data: new TextEncoder().encode(
      JSON.stringify({ name: "Peter", age: 51 })
    ),
  })
  .execute();

const fileAfterChange = await lix.db
  .selectFrom("file")
  .where("path", "=", "/example.json")
  .select([
    "path",
    "data",
    // TODO https://github.com/opral/lix-sdk/issues/348 expose change set id
    // "lixcol_change_set_id",
  ])
  .executeTakeFirstOrThrow();

console.log("File after change:", [
  {
    ...fileAfterChange,
    data: JSON.parse(new TextDecoder().decode(fileAfterChange.data)),
  },
]);

Key points:

  • The JSON plugin identifies exactly what changed (the age property from 50 to 51)
  • History preservation - The previous state remains accessible while the current state is updated
  • No manual commits needed - Unlike Git, changes are captured instantly without explicit commits

File History

With files under change control, you can query their complete history using standard SQL. Every change is preserved and accessible.

import { openLix  } from "@lix-js/sdk";
import { plugin as jsonPlugin } from "@lix-js/plugin-json";

const lix = await openLix({
  providePlugins: [jsonPlugin],
});

const json = {
  name: "Peter",
  age: 50,
};

await lix.db
  .insertInto("file")
  .values({
    path: "/example.json",
    data: new TextEncoder().encode(JSON.stringify(json)),
  })
  .execute();

const fileAfterInsert = await lix.db
  .selectFrom("file")
  .select(["path", "data"])
  .executeTakeFirstOrThrow();

console.log("File after insert:", [
  {
    ...fileAfterInsert,
    data: JSON.parse(new TextDecoder().decode(fileAfterInsert.data)),
  },
]);

// we update the user's age to 51
await lix.db
  .updateTable("file")
  .where("path", "=", "/example.json")
  .set({
    data: new TextEncoder().encode(
      JSON.stringify({ name: "Peter", age: 51 })
    ),
  })
  .execute();

const fileAfterChange = await lix.db
  .selectFrom("file")
  .where("path", "=", "/example.json")
  .select([
    "path",
    "data",
    // TODO https://github.com/opral/lix-sdk/issues/348 expose change set id
    // "lixcol_change_set_id",
  ])
  .executeTakeFirstOrThrow();

console.log("File after change:", [
  {
    ...fileAfterChange,
    data: JSON.parse(new TextDecoder().decode(fileAfterChange.data)),
  },
]);
const activeVersion = await lix.db
  .selectFrom("active_version")
  .innerJoin("version", "active_version.version_id", "version.id")
  .select("version.change_set_id")
  .executeTakeFirstOrThrow();

// Query file history from the current change set
const fileHistory = await lix.db
  .selectFrom("file_history")
  .where("path", "=", "/example.json")
  .where("lixcol_root_change_set_id", "=", activeVersion.change_set_id)
  .orderBy("lixcol_depth", "asc")
  .select(["path", "data", "lixcol_depth"])
  .execute();

console.log(
  "File history:",
  fileHistory.map((f) => ({
    ...f,
    data: JSON.parse(new TextDecoder().decode(f.data)),
  }))
);

Key points:

  • file_history provides access to all previous states of a file
  • Changes are ordered by depth: 0 is the current state, higher numbers are older
  • Each row contains the complete file content at that point in time
  • Standard SQL queries work - filter, join, and analyze history however you need

Further Reading

To learn more about Lix and its capabilities, check out: