# Quickstart

Let an agent change a sample table, watch it get refused when it tries to drop one, edit a row yourself, then undo the run and keep your edit.

You will replay one bad run on a sample database. The agent qualifies 3 leads, then tries to drop a table and is refused. You edit one of those leads yourself. Then you undo the agent's run, and your edit stays.

You need:

- macOS or Linux, a terminal and `sudo`.
- Go 1.26 or newer, and `git`, to build `basalt`.
- Docker, or a Postgres you own and its admin connection string.
- An agent that supports MCP. The steps use Claude Code. [Connect an agent](/start/connect-agent/) covers OpenCode, Codex and a bot.
- A browser that can use a passkey (Touch ID, a security key, or a password manager that stores passkeys).

## 1. Install basalt

There is no release download yet. Build `basalt` from source by following [step 1 of the Install page](/start/install/#1-build-and-install-basalt), then check it:

```sh
basalt version
```

## 2. Start a database and guard it

Pick one:

### Docker

Start a throwaway Postgres, then let BasaltDB set up a sample schema in it.

```sh
docker run -d --name basalt-demo -p 5432:5432 -e POSTGRES_PASSWORD=demo postgres:17
basalt init --demo
```

`basalt init --demo` asks for the admin connection string. It is used once and never stored. For the container above, paste:

```text
postgres://postgres:demo@localhost:5432/postgres
```

### Your own Postgres

Run the same demo on a Postgres you own. It adds the schemas `basalt_demo` and `basalt_meta`, BasaltDB's own roles, and triggers on the demo tables. Nothing else is touched.

```sh
basalt init --demo
```

`basalt init --demo` asks for your admin connection string, the one for the `postgres` user. It is used once and never stored. Supabase shows it under **Connect**. See [Hosts](/guides/hosts/) for which string to pick.

To guard your own tables instead of the sample ones, see [Install](/start/install/).

### What init does

After you give it the connection string, `basalt init --demo`:

1. Creates the schema `basalt_demo` with two tables: `leads` (30 rows) and `lead_notes` (20 rows).
2. Creates an agent named `demo_bot` that can read and change rows in `basalt_demo` and nowhere else.
3. Shows you the setup SQL and asks `Run this SQL as postgres? [y/N]`. Type `y`.
4. Asks you to register a passkey. Open the link it prints, confirm with your passkey, and paste the code the page shows.
5. Starts the engine and prints the command your agent uses.

When it finishes you see:

```text
basalt: guard installed; agent role basalt_agent_demo_bot
basalt: passkey registered for localhost
basalt: demo ready: leads 30, lead_notes 20
```

## 3. Connect your agent

```sh
claude mcp add basalt-demo_bot -- "$(which basalt)" mcp --agent demo_bot
```

If the `claude` command was on your `PATH`, `basalt init --demo` offered to run this for you. Start `claude` in a terminal and run `/mcp`. `basalt-demo_bot` should show as connected. For another agent, see [Connect an agent](/start/connect-agent/).

No database URL goes in the agent's config. The agent never holds one.

## 4. Let the agent work

### Qualify three leads

Send this to your agent:

> Use the basalt-demo_bot tools. Start a run with the intent "Qualify this week's leads". Then mark leads 1, 2 and 3 as QUALIFIED and add 10 to their score. Intent: "Qualify leads 1 to 3".

The agent starts a run, then makes one write. BasaltDB applies it and saves the old values. The result is JSON (the ids here are examples):

```json
{
  "changeId": "CHANGE_ID",
  "runId": "RUN_ID",
  "op": "UPDATE",
  "rows": 3,
  "durationMs": 9,
  "status": "APPLIED"
}
```

The scores of leads 1, 2 and 3 go from 37, 74 and 11 to 47, 84 and 21. A write with no intent is refused with `INTENT_REQUIRED`.

### Try to drop a table

Send this:

> Now drop the old leads table.

The table is not touched. The result is a tool error with `isError: true`:

```json
{
  "code": "STATEMENT_REFUSED",
  "message": "DDL_NOT_ALLOWED: DropStmt",
  "reason": "DDL_NOT_ALLOWED",
  "personMessage": "The agent tried to change the database schema (DDL). Basalt never lets an agent do that. Make the change yourself if you want it.",
  "nextSteps": [
    {
      "actor": "AGENT",
      "action": "STOP",
      "text": "Do not retry and do not look for another way to change the schema."
    },
    {
      "actor": "AGENT",
      "action": "ASK_PERSON",
      "text": "Tell the person which schema change you need; they make it themselves."
    }
  ],
  "docsUrl": "https://docs.basaltvault.app/reference/errors/#statement-refused"
}
```

The refusal is in the ledger too. See [When something is refused](/guides/refused/).

### Get the run id

Ask the agent:

> What is the run id of the run you started?

Copy it. You need it in step 6.

## 5. Edit one lead yourself

Set lead 2 to `WON` with a score of 99. This is a normal write from you, so BasaltDB does not block it or count it as the agent's.

```sql
UPDATE basalt_demo.leads SET status = 'WON', score = 99 WHERE id = 2;
SELECT id, status, score FROM basalt_demo.leads WHERE id <= 3 ORDER BY id;
```

Run them in your host's SQL editor, or in `psql` as your admin role. With the Docker database:

```sh
docker exec basalt-demo psql -U postgres \
  -c "UPDATE basalt_demo.leads SET status = 'WON', score = 99 WHERE id = 2" \
  -c "SELECT id, status, score FROM basalt_demo.leads WHERE id <= 3 ORDER BY id"
```

You see:

```text
UPDATE 1
 id |  status   | score
----+-----------+-------
  1 | QUALIFIED |    47
  2 | WON       |    99
  3 | QUALIFIED |    21
(3 rows)
```

## 6. Undo the run

Replace `RUN_ID` with the id from step 4. Add `--dry-run` to see the plan without undoing anything.

```sh
basalt revert --agent demo_bot --run RUN_ID
```

BasaltDB prints one line per row it would restore or leave alone, then a link to the undo, and waits for your signature:

```text
restore    basalt_demo.leads {"id": 1}: score: 47 -> 37, status: "QUALIFIED" -> "NEW"
restore    basalt_demo.leads {"id": 3}: score: 21 -> 11, status: "QUALIFIED" -> "NEW"
skip       basalt_demo.leads {"id": 2}: CHANGED_AFTER
basalt: 2 rows to restore, 1 left as they are (people win)
basalt: revert REVERT_ID expires 2026-11-15 08:21 UTC. Open this link on a device with your passkey, check the rows, and sign:
http://localhost:8787/reverts/REVERT_ID#s=...
basalt: waiting for your signature on the revert page (or paste the code it shows here)...
```

Leads 1 and 3 go back to how they were. Lead 2 stays, because you changed it after the agent (`CHANGED_AFTER`). BasaltDB leaves alone any row your team or your app changed since.

## 7. Approve the undo

`basalt revert` is now waiting for you, as its last line says. Nothing is undone yet.

Open the link `basalt revert` printed. If you lost it, stop the command with Ctrl-C and run it again with `--open`. That prepares a fresh undo and opens its link in your browser.

```sh
basalt revert --agent demo_bot --run RUN_ID --open
```

Check the rows on the page, then approve the undo there. The command finishes by itself:

```text
basalt: reverted: 2 rows restored, 1 left as they are
```

If the page cannot reach the command, it shows a code. Paste the code into the terminal and the undo applies the same way.

## 8. Check the result

Run this query. Leads 1 and 3 are back to their original status and score. Lead 2 still has your edit.

```sql
SELECT id, status, score FROM basalt_demo.leads WHERE id <= 3 ORDER BY id;
```

You see:

```text
 id | status | score
----+--------+-------
  1 | NEW    |    37
  2 | WON    |    99
  3 | NEW    |    11
(3 rows)
```

## Clean up

`basalt uninstall` removes BasaltDB from the database, including the sample schema `basalt_demo`. Then remove the engine service. See [Remove BasaltDB](/guides/remove/).

```sh
basalt uninstall
sudo basalt service uninstall
```

With Docker, you can then remove the container too.

```sh
docker rm -f basalt-demo
```

## Next

- [Install](/start/install/): guard your own tables.
- [See what it did](/guides/see-what-it-did/): read the record with `basalt log`.
- [Undo a run](/guides/undo/): the full guide to undoing a run.
- [Runs](/concepts/runs/): why every write carries a reason.
