---
title: Quickstart
description: Install Lakefront, start the catalog Postgres, and write and read your first board.
---

Lakefront requires Bun 1.2 or later. The package ships TypeScript source, and Node cannot run it.

## 1. Install

```package-install
lakefront
```

## 2. Start the catalog Postgres

The catalog is where writes commit: the WAL table lives here, and so does the DuckLake truth store's metadata. A plain Postgres 17 is all the quickstart needs. Parquet files go to a local directory, so there is no object store yet; a multi-node deployment adds S3, covered in [the cluster guide](/guides/cluster).

```yaml compose.yaml
services:
  pg:
    image: postgres:17
    environment:
      POSTGRES_USER: lakefront
      POSTGRES_PASSWORD: lakefront
      POSTGRES_DB: lakefront
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U lakefront -d lakefront"]
      interval: 2s
      timeout: 3s
      retries: 30
```

```sh
docker compose up -d
```

## 3. Write and read a board

There is no `CREATE TABLE`. A board is created on its first write, and its columns are inferred from the data. [The schema guide](/guides/schema) covers how inference works and how to declare a schema instead.

```ts quickstart.ts
import { open } from "lakefront";

await using engine = await open({
  postgres: "postgres://lakefront:lakefront@127.0.0.1:5432/lakefront",
  data: "./data/lake/",
  topology: "single-node",
});

await engine.insertMany("tasks", [
  { id: "task-1", title: "Ship the thing", status: "open" },
  { id: "task-2", title: "Fix the bug", status: "open" },
]);

const read = (
  await engine.query("tasks", "SELECT id, title, status FROM {{table}} ORDER BY id")
).unwrap("read");
console.log(read.result.rows);
```

```sh
bun run quickstart.ts
```

Two required fields, `postgres` and `data`, and defaults for everything else. `Engine` is `AsyncDisposable`, so `await using` closes the connection pool, the WAL, and the background flusher when the script's scope ends.

## 4. Read your own write

Every write returns the watermark it committed at. Hand it back as `minWatermark`, and the read waits for a replica that has caught up to it rather than risking a stale one:

```ts
const watermark = (await engine.update("tasks", "task-2", { status: "done" })).unwrap("update");

const fresh = (
  await engine.query("tasks", "SELECT status FROM {{table}} WHERE id = $1", {
    params: ["task-2"],
    minWatermark: watermark,
  })
).unwrap("read");
```

The watermark model behind this is on the [consistency page](/concepts/consistency).

## Where next

- [The tutorial](/guides/tutorial) walks the same board through schema evolution, a declared strict board, a cross-board transaction, watch, and HTTP serving.
- [Concepts](/concepts) covers the design behind what just ran: durability, write paths, routing.
