Skip to content
BasaltDBBasaltDB
Star

Quickstart

View as Markdown

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 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).

There is no release download yet. Build basalt from source by following step 1 of the Install page, then check it:

basalt version

Pick one:

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

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:

postgres://postgres:demo@localhost:5432/postgres

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:

basalt: guard installed; agent role basalt_agent_demo_bot
basalt: passkey registered for localhost
basalt: demo ready: leads 30, lead_notes 20
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.

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

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):

{
  "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.

Send this:

Now drop the old leads table.

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

{
  "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.

Ask the agent:

What is the run id of the run you started?

Copy it. You need it in step 6.

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.

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:

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:

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

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

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:

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.

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.

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:

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.

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

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

You see:

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

basalt uninstall removes BasaltDB from the database, including the sample schema basalt_demo. Then remove the engine service. See Remove BasaltDB.

basalt uninstall
sudo basalt service uninstall

With Docker, you can then remove the container too.

docker rm -f basalt-demo

BasaltDB · Apache-2.0