# BasaltDB — full documentation export Generated at build time from documentation sources. Site: https://docs.basaltvault.app --- # Page not found URL: https://docs.basaltvault.app/404/ # Page not found Check the URL or start from the [overview](/). --- # Approvals URL: https://docs.basaltvault.app/concepts/approvals/ > **Note:** The approval flow is being simplified. This page describes how it works at launch. A write that the [rules](/concepts/rules/) hold does not run until a person approves it. That covers a write of 11 to 50 rows and any `DELETE`. An approval counts only with the person's passkey signature, and Postgres checks that signature. ## The flow 1. The agent sends a write that the rules hold. The engine checks it will run the same way twice, runs it in a transaction to collect the exact rows it would touch, cascaded rows included, then rolls the transaction back. A preview can never commit. 2. The engine records the change as held (`APPROVAL_PENDING`) and returns `APPROVAL_REQUIRED` to the agent. The result holds the approval id, the time it expires, a one-line summary of the write, a link and a command. 3. The agent shows you the message, with the link on its own line. The summary reads like `Delete 1 row in public.leads + 2 cascaded rows (public.lead_notes 2)`. 4. You open the link, check the rows and approve with your passkey. If you lose the link, run the command again: ```sh basalt approve --agent NAME --open APPROVAL_ID ``` 5. The agent repeats the same write with the approval id. The write applies once Postgres confirms the signature, and the agent gets `APPLIED`. By default the agent waits up to 90 seconds on that repeat for you to approve, then tells you the write is waiting. A later repeat applies at once. ## What the approval is bound to The signature covers exactly what you saw: - the statement and its parameters; - for every affected row, cascades included: the table, the operation, the key, the full old row, and the new values of the columns the statement sets. Defaults, identity values and values set by your app's triggers are not covered. A held write that contains a value that changes between transactions, such as `now()`, is refused with `STATEMENT_REFUSED` and the reason `NOT_REPEATABLE`. The agent should pass such a value as a parameter. When the write is applied, Postgres recomputes the statement, the parameters, the row count and a hash of the rows. If any differ from what you signed, the write does not apply and a new preview is needed. ## Who can approve Only you. The agent can open the link but cannot sign. No PIN is typed into the chat, because anything typed to the agent passes through the model and its vendor's logs. The signature is a WebAuthn passkey signature (ES256, P-256). The public key of your passkey is stored in `basalt_meta` when you run `basalt init`. Postgres verifies each signature. The engine, the CLI, the page and any relay that carries the signed data are not trusted to approve: a signature they did not get from your passkey is refused. The pages that ask for your signature are static. They load no third-party code and send nothing over the network. By default the engine serves them on `http://localhost:8787`. ## Expiry and codes An approval expires 24 hours after the hold. No answer means blocked. | Code | Meaning | | ------------------- | ------------------------------------------------------------------------------------ | | `APPROVAL_REQUIRED` | The write is held. The result carries the link. | | `APPROVAL_EXPIRED` | Nobody approved within 24 hours. | | `APPROVAL_INVALID` | The approval does not exist, is used, belongs to another agent, or the rows changed. | See [Errors](/reference/errors/) for what each one tells the agent to do. ## Next - [Rules](/concepts/rules/): which writes are held. - [Passkeys](/guides/passkeys/): add, check and remove passkeys. - [Undo](/concepts/undo/): a person signs an undo the same way. --- # Architecture URL: https://docs.basaltvault.app/concepts/architecture/ BasaltDB has two parts. The engine runs next to your agent. The guard runs inside your Postgres. Your data stays where it is. ![Your agent talks MCP to the BasaltDB engine, which connects to your Postgres with the agent role. Inside your Postgres, the guard checks every write and the ledger in basalt_meta keeps the old values.](https://docs.basaltvault.app/diagrams/engine-overview.png) Your agent talks MCP to basalt mcp, which hands its requests to the BasaltDB engine next to it. The agent never holds a database URL. The engine connects to your Postgres with the agent role. Inside your Postgres the guard checks every write and refuses the risky ones, and the ledger in the basalt_meta schema keeps the old values so a job can be undone. ## The engine The engine is one Go program. It runs as its own OS user, and the agent reaches it through a small program, `basalt mcp`, which relays MCP requests to it. The agent holds no database URL. Only the engine holds the agent's scoped credential. The engine reads each statement, refuses what is never allowed, records reads, and passes the rest to Postgres. It also serves the pages where you approve and undo, on `http://localhost:8787` by default. ## The guard The guard is the `basalt_meta` schema in your Postgres, with its own roles and triggers. Agent roles have no rights to change tables. The triggers act only for agent roles. They require the reason, save old and new rows in the same transaction, and count rows per action against the [rules](/concepts/rules/), cascades included. Because the rules live in Postgres, they hold on any path. An agent that opens a direct connection with its credential meets the same checks. If the guard cannot record or evaluate an agent's write, the write fails. Writes by people and by your app are never blocked. They pay one role check. The guard also checks passkey signatures for approvals and undos, inside Postgres. Whatever carries a signature cannot forge one. ## What is where | Piece | Where it runs | Holds | | ------------------------------------ | ----------------------------------- | ----------------------------- | | `basalt mcp` | Next to your agent | No credential | | Engine | Next to your agent, own OS user | The agent's scoped credential | | Guard, ledger and passkey public key | Inside your Postgres, `basalt_meta` | Records, old values, rules | | Your tables | Your Postgres | Your data, unchanged | The admin credential is used once, by `basalt init`, and is never stored. ## Where it stops If the agent shares an OS user with the engine, it can reach the engine's credential. In that same-user mode writes are still guarded by the triggers, but reads over a direct connection are not recorded. An agent with your admin credential, or with `sudo` on the engine's machine, is outside what BasaltDB protects against. See [Limits](/limits/). ## Remove it BasaltDB adds the `basalt_meta` schema, its own `basalt_*` roles, and triggers and policies on the tables you guard. `basalt uninstall` removes all of it. See [Remove BasaltDB](/guides/remove/). ## Next - [Install](/start/install/): set up the guard on your Postgres. - [Rules](/concepts/rules/): what the guard enforces. --- # The ledger URL: https://docs.basaltvault.app/concepts/ledger/ The ledger is the record of every action an agent asks for. It is what lets BasaltDB preview a change, stop one, ask you, and undo it. ## What one record holds One record per action, whether it was applied, held or refused: - **Who.** The agent, its run and its task. - **Why.** The intent the agent gave. - **What.** The statement and its parameters, the kind of action, the rows touched and how long it took. - **Before and after.** For each row written, the old values and the new ones, cascaded rows included. - **The decision.** `ALLOW`, `HOLD` or `REFUSE`, and the rule behind it. - **What it saw.** For updates and deletes, the version of the row the run had read. Reads through BasaltDB are recorded too. Each read keeps the rows returned and the version of each, so the ledger knows what the agent was looking at when it wrote. ## Statuses | Status | Meaning | | ------------------ | -------------------------------------------------- | | `APPLIED` | The change ran. | | `APPROVAL_PENDING` | The change is held and waits for you. | | `REFUSED` | The change did not run. The record has the reason. | | `APPROVAL_EXPIRED` | The hold ran out after 24 hours. | | `REVERTED` | Every row of it was undone. | The full shape is on [Change record](/reference/change-record/). ## Where it lives In the `basalt_meta` schema inside your own Postgres. BasaltDB has no server of its own that holds your data. `DROP SCHEMA basalt_meta CASCADE` removes it. See [Remove BasaltDB](/guides/remove/). ## How long it is kept - Old values: **30 days**. This is how long an undo can restore a row. - Everything else about each action: **1 year**. ## Tamper evidence Each ledger row is chained to the one before it with a hash. `basalt log` checks the chain and warns if it is broken. The ledger is append-only: an undo is a new record and never edits an old one. ## Reading it ```sh basalt log --run RUN_ID ``` One line per action, with the rows it wrote indented below: ```text 2026-11-15T07:20:47Z APPLIED UPDATE basalt_demo.leads 3 rows 9ms 7c3e4f0a-5b1d-4c8e-9a62-0d1f2e3a4b5c "Qualify leads 1 to 3" UPDATE basalt_demo.leads (blind write) {"id":1} score 37 -> 47, status NEW -> QUALIFIED UPDATE basalt_demo.leads (blind write) {"id":2} score 74 -> 84, status NEW -> QUALIFIED UPDATE basalt_demo.leads (blind write) {"id":3} score 11 -> 21, status NEW -> QUALIFIED ``` Consecutive reads collapse into one line, such as `3 reads, 42 rows: leads (2), lead_notes (1)`. Add `--full` for whole before and after rows. `--json` gives the raw records. An agent reads its own runs with `run_history` and `list_runs`. For a table the agent cannot read, the record shows `values hidden` instead of the rows. After 30 days it shows `values pruned`. ## Next - [See what it did](/guides/see-what-it-did/): `basalt log` in detail. - [Runs](/concepts/runs/): how actions are grouped. - [Undo](/concepts/undo/): what the old values are for. --- # Rules URL: https://docs.basaltvault.app/concepts/rules/ BasaltDB decides every action from how many rows it touches and what kind of statement it is. These are the defaults. They apply to every agent. ## The defaults | What the agent asks for | What happens | | ---------------------------------------------------------------------------- | ---------------------------------------------------------- | | A read (`SELECT`) | Allowed, within a row limit and a time limit. Recorded. | | An insert or an update of 1 to 10 rows | Goes through. The reason is recorded and old values saved. | | An insert or an update of 11 to 50 rows | Held until you approve it, with a preview of the rows. | | Any `DELETE` of up to 50 rows | Held until you approve it, with a preview of the rows. | | `CREATE`, `ALTER`, `DROP`, `TRUNCATE` (DDL) | Refused. Agents can't drop or change tables. | | More than 50 rows in one action | Refused. | | Several statements at once, `COPY`, data-changing `WITH`, unlisted functions | Refused. | ## How rows are counted Rows are counted per action, and cascaded rows count too. A delete of 5 rows that cascades into 50 more counts as 55 and is refused. A delete that cascades into 3 rows counts as 8 and is held. The count is made in your Postgres by the guard, not only by the engine. That is why a direct connection with the agent's credential meets the same limits. ## What an agent sees A write that goes through returns `"status": "APPLIED"` and the number of rows: ```json { "status": "APPLIED", "rows": 3 } ``` A write that is held does not run. The agent gets `APPROVAL_REQUIRED` with a link for you, and shows it to you. A write that is refused is a tool error with a code and, for a refused statement, a reason: ```json { "code": "STATEMENT_REFUSED", "reason": "DDL_NOT_ALLOWED" } ``` A held write is `APPROVAL_REQUIRED`, too many rows is `ROW_LIMIT_EXCEEDED`, and DDL is `STATEMENT_REFUSED` with the reason `DDL_NOT_ALLOWED`. [Errors](/reference/errors/) lists every code and what to do about it. ## Why these numbers - **Up to 10 rows** covers the everyday work an agent does on its own: fix a record, add a note, qualify a lead. - **11 to 50 rows** is a change large enough that you should look at the exact rows first. - **Deletes** are never applied without you, because a deleted row is the hardest thing to notice later. - **Over 50 rows** belongs in a migration or a data pipeline that a person runs. A loop of small writes is not blocked. It lands in one run and you can undo it at once. A held write waits for your approval. If you do not answer within 24 hours, it is blocked. See [Approvals](/concepts/approvals/). ## What the rules do not catch A change that is valid, touches one row and is wrong for your business goes through. It is recorded and you can undo it later, but nothing stops it first. See [Limits](/limits/). ## Next - [Reference: limits](/reference/limits/): the numbers and defaults in one table. - [When something is refused](/guides/refused/): what to do when an agent reports a refusal. - [Undo](/concepts/undo/): what happens after a change went through. --- # Runs URL: https://docs.basaltvault.app/concepts/runs/ A run is one job your agent does, with one reason you can read. It is the unit you review, and the unit you undo. ## How a run starts The agent starts a run with `run_start` and a sentence that says why: ```text run_start intent: "Qualify this week's leads" ``` BasaltDB returns a run id. The agent passes it to every `query` and `write` that belongs to the job. If a write arrives without a run id, BasaltDB starts a run of one write for it. The intent is required and holds up to 500 characters. Write it for a person: someone will read it next to the rows it changed. ## The reason on every write Each `write` also carries its own intent. A write without one is refused: ```json { "code": "INTENT_REQUIRED" } ``` The refusal does not depend on the engine. The guard inside your Postgres checks for the intent too. If an agent finds a way around the engine and writes directly with its own credential, Postgres refuses the write. ## What a run records For each action in a run, BasaltDB records the reason, the statement, the rows it touched and their old values. It records reads as well. Each read stores the version of the rows the agent saw. Reads are what make this rule possible: an update to a row that changed after the agent read it is refused with `STALE_READ`. The agent has to read the row again before it writes. A row the agent never read can still be written. That is called a blind write, and the record marks it. `basalt log --run RUN_ID` shows a run as one line per action, with the rows it wrote indented below: ```text 2026-11-15T07:20:47Z APPLIED UPDATE basalt_demo.leads 3 rows 9ms 7c3e4f0a-5b1d-4c8e-9a62-0d1f2e3a4b5c "Qualify leads 1 to 3" UPDATE basalt_demo.leads (blind write) {"id":1} score 37 -> 47, status NEW -> QUALIFIED UPDATE basalt_demo.leads (blind write) {"id":2} score 74 -> 84, status NEW -> QUALIFIED UPDATE basalt_demo.leads (blind write) {"id":3} score 11 -> 21, status NEW -> QUALIFIED ``` ## Tasks across agents A task groups runs. Pass a `taskId` to `run_start` to join a task another agent started, even one from a different vendor. You can undo a whole task at once. See [Undo](/concepts/undo/). ## Why runs matter - You read a run's reason first, then its rows. - You undo a whole run at once. A loop of small writes is not blocked, and it all lands in one run. - An agent can find its own runs with `list_runs`, so you can say "undo what you did yesterday on leads". ## Next - [Rules](/concepts/rules/): what a run may and may not do. - [The ledger](/concepts/ledger/): everything BasaltDB records about each action. - [MCP tools](/reference/mcp-tools/): `run_start`, `query`, `write` and the others. --- # Undo URL: https://docs.basaltvault.app/concepts/undo/ Undo puts back what an agent changed. It never overwrites what someone else changed since. ## People win BasaltDB restores a row only if nobody has written it after the agent's last change. If your team or your app changed since, the row stays as it is. Undo skips it, lists it, and restores the rest. ![Undo restores the agent's rows and keeps rows changed after the agent: leads 1 and 3 get their old values back, lead 2, changed by your app after the agent, is kept as it is.](https://docs.basaltvault.app/diagrams/conflict-resolution.png) When you undo a job, BasaltDB puts back the agent's changes: updated rows get their old values and deleted rows come back. Rows your team or your app changed after the agent stay as they are, and BasaltDB lists them instead of overwriting them. Take one job where the agent changed leads 1, 2 and 3. Your app then marked lead 2 as won. When you undo the job: ```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: reverted: 2 rows restored, 1 left as they are ``` A partial undo is a normal result. The command exits with `0`, and every skipped row is named with its reason. ## Why a row is skipped | Reason in the list | Why | | ------------------ | ---------------------------------------------------------------------------- | | `CHANGED_AFTER` | Someone wrote the row after the agent: your team, your app or another agent. | | `ROW_GONE` | The row was deleted since, or the guard cannot see it. | | `KEY_TAKEN` | Undo would put a row back, and a row with that key exists now. | | `ALREADY_REVERTED` | An undo of this change already restored it. | | `VALUES_PRUNED` | The values to put back have been pruned. | | `SCHEMA_CHANGED` | The table, a column of the old row or the primary key changed. | | `CASCADE` | Putting the row back would change a row that is not part of the undo. | | `CONSTRAINT` | The restoring write fails a constraint when BasaltDB tries it. | ## What undo covers You can undo one change, one run, or one task. `basalt revert` prepares the work of the agent you name, and you can widen it to other agents' changes in the task on the signing page. Rows an agent updated get their old values. Rows it deleted come back, including rows removed by a cascade. Rows it inserted are deleted. A row the agent inserted and then deleted is left out. Row limits do not apply to an undo. A large undo is applied in batches. ## The 30-day window BasaltDB keeps old values for 30 days. After that the record of the change stays, but the values are pruned, and undo skips those rows. The rest of the ledger is kept for a year. Undo does not reach outside your database either: an email already sent stays sent. ## Your signature An undo changes your data, so a person has to sign it. You review the rows, then confirm with a passkey: Touch ID, a security key, or your phone. Postgres checks the signature before it restores anything. - An agent can preview an undo and prepare one for you. It cannot sign. - A prepared undo expires after 1 hour if it is not signed. - The undo is recorded as a new change that names you. History is never rewritten, and you can undo an undo. ## Next - [Undo a run](/guides/undo/): the steps, with `basalt revert`. - [The ledger](/concepts/ledger/): where the old values are kept. - [Limits](/limits/): what undo cannot do. --- # FAQ URL: https://docs.basaltvault.app/faq/ ## Is BasaltDB a database? BasaltDB is the action ledger for AI agents on a Postgres you already own. It guards a database you run and keeps a record of what agents did there. Your data stays in your Postgres. BasaltDB adds the `basalt_meta` schema, its own roles, and triggers and policies on the tables you guard. `basalt uninstall` removes all of it. See [What is BasaltDB](/start/what-is-basaltdb/). ## Does it need a hosted service? An engine you run on your own computer needs nothing hosted. The engine runs next to your agent, the guard runs inside your Postgres, and the pages where you approve and undo are served from your machine. An engine your bot runs on its own computer is different. Its approval and undo links open on the hosted BasaltVault portal, `portal.basaltvault.app`, and you paste the code the page shows back into the terminal. The hosted relay that would carry short links to such an engine is not finished, so links today are long links. See [A bot on its own computer](/guides/bot-own-computer/) and [Architecture](/concepts/architecture/). ## Which Postgres hosts does it work with? Supabase, RDS and a Postgres you host yourself. It needs a role that can create roles. Connect directly or through a pooler in session mode. A transaction pooler has not been checked. Each host has its own notes in [Hosts](/guides/hosts/). ## Which agents does it work with? Agents that speak MCP. There are setup steps for OpenCode, Claude Code, Codex and a bot on its own computer in [Connect an agent](/start/connect-agent/). ## Is this just a prompt or an MCP wrapper? The limits are enforced by roles and triggers inside your Postgres. They hold on any path, so an agent that opens a direct connection with its own credential meets the same checks. A prompt asks the model to behave. The guard refuses the statement. See [Rules](/concepts/rules/). ## What if the agent does something valid but wrong? A one-row update to the wrong value is valid SQL and goes through. It is saved with the reason the agent gave and the values it replaced, so you can see it and undo it. Nothing stops it first. Changes of 11 to 50 rows and every delete are held for you, and more than 50 rows never run. See [What BasaltDB won't catch](/limits/). ## What if my team or my app changed a row after the agent? Undo leaves that row as it is. It restores the rest and lists the rows it kept. See [Undo](/concepts/undo/). ## What about prompt injection? BasaltDB limits the damage and leaves a trail. An injected instruction cannot drop a table, change more than 50 rows in one action, or approve its own change, and every action is recorded with its reason. It does not stop an agent from passing on data it has already read. See [What BasaltDB won't catch](/limits/). ## Can I give an agent write access to production? For internal tools, back-office data and small-business bots, yes, and that is what BasaltDB is for. It is not built for time-critical or mission-critical systems, or for teams that need compliance tooling. If a wrong write costs more than undoing it can fix, keep the agent read-only. ## Is it free? What does BasaltVault cost? BasaltDB is free and released under Apache-2.0. BasaltVault is a separate paid service for many agents and machines. It is not open yet. Prices are not published. See [License](/license/). ## How do I remove it? Run `basalt uninstall` on your own computer. It drops the `basalt_meta` schema and the `basalt_*` roles. Your tables and rows stay as they are. The record of what agents did goes with the schema, so you cannot undo agent runs afterwards. See [Remove BasaltDB](/guides/remove/). ## Where is my data? In your Postgres. Your tables are not copied or moved. The ledger and old values are in the `basalt_meta` schema in the same database. The engine's config and the agent's scoped credential sit on the machine where the engine runs. An engine you run on your own computer sends nothing to a service of ours. If your bot runs the engine, its approval and undo links open on the hosted BasaltVault portal. The AI your agent uses still sees what the agent reads. See [Architecture](/concepts/architecture/). ## Is it ready to use? Everything these docs describe works today when you build BasaltDB from source. The honest list of what it does not do is on [What BasaltDB won't catch](/limits/). ## Where do I report a security problem? Privately, through GitHub. See [Security](/security/). --- # A bot on its own computer URL: https://docs.basaltvault.app/guides/bot-own-computer/ Use this page when your bot runs on a machine you cannot log in to, such as a server or a chat bot's host, and you only talk to it in a chat. The bot installs and runs the engine there. You run one command on your own computer, with the admin connection string. The admin connection string never reaches the bot's machine. ## Who does what | Where | Who | What | | ----------------- | ---- | ----------------------------------------------------------------------------- | | The bot's machine | Bot | Runs `basalt init --remote`. Gets a setup code. Starts the engine afterwards. | | The chat | Both | The bot shows you the setup code and its fingerprint. You tell it when done. | | Your own computer | You | Runs `basalt init --setup-code` with your admin connection string. | | Your Postgres | You | Receives the guard, the agent role and its tables, in one transaction. | ## Before you start - `basalt` is installed on the bot's machine and on your own computer. See [Install](/start/install/). Your own computer must be macOS or Linux. - You know the Postgres connection string without its password. Supabase shows it with a `[YOUR-PASSWORD]` placeholder. See [Hosts](/guides/hosts/) for which string to pick. - You can reach your Postgres from your own computer. The bot's machine needs to reach it too, because the engine connects from there. ## 1. The bot makes a setup code The bot runs this on its own machine. The connection string has no password in it. ```sh basalt init --remote --agent my_bot --operated-by=agent \ --write public.leads --read public.lead_notes \ --database-url "postgresql://postgres@HOST:5432/postgres" ``` - `--operated-by=agent` tells BasaltDB that the bot, not you, runs machine steps such as starting the engine. - `--write` lists tables the bot may read and change. `--read` lists tables it may only read. - If the connection string has a password, the command refuses it, does not write it anywhere, and tells you to change your database password. Never paste it to a bot. - The command does not ask for an admin credential. It ignores `BASALT_ADMIN_DATABASE_URL`. It has no prompt. The bot generates the agent's password and keeps it in a file only it can read. Only a verifier of that password goes into the setup code. Nobody can log in with a verifier. The command prints the setup code in a block of its own, then its fingerprint: ```text basalt-setup.1.eJxVjMEKwjAQRP9lzhJMPWjzK6WHEFcb... Setup fingerprint: 3f9a 21c0 7d44 e1b2 90ac ``` The code is valid for 24 hours and works once. While the code waits, running the command again prints the same code. A change to the tables, or `--new-code`, makes a new code and retires the old one. An expired code is replaced by running the command again. The bot shows you the code and the fingerprint in the chat. Use a code block, so the chat does not read the underscores as emphasis. ## 2. You run the code on your own computer ```sh basalt init --setup-code SETUP_CODE ``` It asks for the admin connection string at a hidden prompt, or reads `BASALT_ADMIN_DATABASE_URL`. It is used once and never written anywhere. Before it changes anything, the command checks the code and shows you a summary: 1. The code is whole, not expired, and not dated more than 5 minutes ahead of your computer's clock. 2. Your connection string names the same database as the one the bot's engine connects to. A mismatch is refused, not asked about. 3. It shows the fingerprint. **Check that it matches the fingerprint your bot gave you in the chat.** If it does not, stop and ask the bot for a new code. A code someone else posted in the chat would show a different one. 4. It says what will happen, in fixed words. A first setup reads `This creates the agent my_bot (role basalt_agent_my_bot). Its password comes from the code: only whoever made this code can log in as basalt_agent_my_bot.` 5. It lists what the bot can do: read and change rows in the tables you named, up to 10 rows at a time without asking you, 11 to 50 with your approval, never more. It cannot change your tables' structure. Tables outside `public` are flagged. 6. It asks the row-level security question for each table that hides its rows, as `basalt init` does. See [Hosts](/guides/hosts/#row-level-security). 7. It shows the SQL and asks `Run this? [y/N]`. Add `--yes` to skip the question, or `--dry-run` to print everything and run nothing. Answer `y`. The setup runs in one transaction, so it is all or nothing: every table or none. If a step fails, your database is as it was. If no passkey is registered, the command then opens a page to register one. Open it, confirm with your passkey and paste the code the page shows. See [Passkeys](/guides/passkeys/). Add `--add-passkey` to register another. It ends with: ```text Done. Tell my_bot to check its status. ``` ## 3. The bot starts its engine `basalt init --remote` does not install or start the engine. The bot does that on its own machine, by running `basalt engine --config PATH`, or by installing it as a service. Until your setup has run, the engine answers every call with `UNAVAILABLE` and the reason `SETUP_PENDING`, and tells the bot to ask you to run the code. When the engine sees that your setup ran, it serves the bot normally. It needs no restart. ## 4. Tell the bot Tell the bot in the chat that you ran the code. It calls its `status` tool and confirms it is ready. You can also check on the bot's machine: ```sh basalt status --agent my_bot ``` ## Change something later Any change after the first setup goes the same way: the bot makes a new code, and you run it on your own computer. | You want to | The bot runs | You run | | --------------------------- | --------------------------------------------------------------------------- | ------------------------------- | | Add or remove a table | `basalt init --remote` with the new `--write` and `--read` lists | `basalt init --setup-code CODE` | | Give the bot a new password | `basalt init --remote --new-code` | `basalt init --setup-code CODE` | | Upgrade the guard | `basalt init --remote`, which makes an upgrade code when the guard is older | `basalt init --setup-code CODE` | `basalt upgrade` refuses on an engine your bot runs, because it would ask for your admin connection string on the bot's machine. See [Upgrade](/guides/upgrade/). ## If it does not work Messages you can see on your own computer: | Message | What to do | | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `This setup code expired on