Logging

Lix includes a lightweight, built-in logging system that writes structured data directly into Lix. This allows you to capture, query, and version logs alongside your application state, making them perfect for audit trails, debugging breadcrumbs, and collaborative session histories.

Quick Start

Add a log entry by inserting a row directly into the log table.

import { openLix } from "@lix-js/sdk";

const lix = await openLix({});

// The `id` and `timestamp` are automatically generated on insertion.
await lix.db
  .insertInto("log")
  .values({
    key: "app_boot_success",
    level: "info",
    message: "Application booted and ready.",
    payload: { version: "1.4.0", mode: "production" },
  })
  .execute();

When to Use Lix Logging

Use Lix for logs that are part of your application's state. Because logs are stored and versioned in the repository, they are queryable, shareable, and available long after the original process has exited.

Use Lix Logging For...Use console.log For...
User-facing logs (errors, activity)Developer-only diagnostics
Persistent state to be queried & syncedEphemeral data for immediate inspection
Testable application logicDebugging the current execution

Querying Logs

Logs are stored in the log table and can be queried with the same Kysely API you use for other entities.

const recentErrors = await lix.db
  .selectFrom("log")
  .selectAll()
  .where("level", "=", "error")
  .orderBy("timestamp", "desc")
  .limit(10)
  .execute();

Since logs are part of change control, you can inspect them at different points in history, diff them between commits, and sync them across clients.

Common Use Cases

A primary use case for Lix Logging is to create a queryable history of events that can be surfaced to end-users in your application's UI.

Displaying Errors to Users

Instead of only logging errors to the console where they are lost, you can write them to Lix. This allows you to build an in-app "Error History" or "Activity" panel, giving users or support staff visibility into what went wrong.

1. Log an error when it occurs:

async function handleSubmit(formData) {
  try {
    await submitData(formData);
  } catch (error) {
    await createLog({
      lix,
      key: "form_submit_failed",
      level: "error",
      message: "Submission failed",
      payload: { message: error.message, fields: Object.keys(formData) },
    });
    // Also notify the user immediately
  }
}

2. Query the logs to display in the UI:

You can create a React component, for example, that queries and displays all error logs.

function ErrorHistoryPanel({ lix }) {
  const [errors, setErrors] = useState([]);

  useEffect(() => {
    const fetchErrors = async () => {
      const result = await lix.db
        .selectFrom("log")
        .where("level", "=", "error")
        .orderBy("timestamp", "desc")
        .limit(50)
        .selectAll()
        .execute();
      setErrors(result);
    };

    fetchErrors();
  }, [lix]); // Re-run if lix instance changes

  return (
    <div>
      <h3>Error History</h3>
      <ul>
        {errors.map((error) => (
          <li key={error.id}>
            <strong>{error.key}</strong>: {error.message ?? "No message"} (
            {error.timestamp})
            {error.payload ? (
              <pre>{JSON.stringify(error.payload, null, 2)}</pre>
            ) : null}
          </li>
        ))}
      </ul>
    </div>
  );
}

Querying Payload Fields

Because payload stores JSON, you can filter logs by payload fields using SQLite's json_extract helper through Kysely's sql tagged template.

import { sql } from "@lix-js/sdk";

const uploadFailures = await lix.db
  .selectFrom("log")
  .selectAll()
  .where("level", "=", "error")
  .where(sql`json_extract(payload, '$.operation')`, "=", "file_upload")
  .where(sql`json_extract(payload, '$.retryable')`, "=", 1)
  .orderBy("timestamp", "desc")
  .execute();

// uploadFailures now contains only error logs whose payload.operation is "file_upload"
// and payload.retryable === true.

Best Practices

  • Use structured snake_case keys: This makes filtering predictable (e.g., checkout_form_submit, checkout_form_error).
  • Store meaningful levels: Use levels that match your observability pipeline (e.g., info, warn, error, debug).
  • Prune test data: In long-running test suites, remember that the log table grows. Prune it if it's not relevant to your test assertions to keep fixtures small.

Schema

The default log table provides a minimal, effective schema.

ColumnTypeDescription
idstringA unique identifier for the log entry.
timestampstringISO 8601 timestamp of when the log was created.
keystringA structured, snake_case key for filtering.
levelstringThe log level (e.g., info, error).
messagestring?Optional descriptive message for the log.
payloadJSONStructured payload for storing queryable data.