> ## Documentation Index
> Fetch the complete documentation index at: https://docs.concatflow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a read-only account

> The agent only serves an account that cannot write. Here is how to create one on each database.

Give the agent its own account, with `SELECT` and nothing else. The agent checks this itself on your machine, every time it starts. If the account can write anywhere in the database, the agent stops with an error and serves nothing.

## PostgreSQL

Run as an administrator. Replace `app` with your database name.

```sql theme={"system"}
CREATE ROLE concatflow_reader LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE app TO concatflow_reader;
GRANT USAGE ON SCHEMA public TO concatflow_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO concatflow_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO concatflow_reader;
```

To expose more schemas, repeat the last three lines for each one:

```sql theme={"system"}
GRANT USAGE ON SCHEMA sales TO concatflow_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA sales TO concatflow_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA sales GRANT SELECT ON TABLES TO concatflow_reader;
```

> [!NOTE]
>
> `ALTER DEFAULT PRIVILEGES` covers tables created later **by the role that runs it**. If another role creates your tables, add `FOR ROLE <that_role>` after `ALTER DEFAULT PRIVILEGES`.

Concatflow only offers the schemas this account can read. A schema without `USAGE`, or without a single readable table, does not appear.

## MariaDB and MySQL

In MariaDB and MySQL, each database is a schema. Grant `SELECT` on each one you want to expose:

```sql theme={"system"}
CREATE USER 'concatflow_reader'@'%' IDENTIFIED BY 'change-me';
GRANT SELECT ON app.* TO 'concatflow_reader'@'%';
```

Replace `'%'` with the address the agent connects from if you want to restrict it.

> [!WARNING]
>
> On MySQL 8, agent `0.1.0` cannot log in with the default `caching_sha2_password` plugin. See [MySQL 8 authentication fails](https://docs.concatflow.com/private-databases/troubleshooting.md#mysql-8-authentication-fails).

## What the agent checks

The check covers the whole database, not only the schemas you expose. An account that can write to a table you never expose can still write.

**PostgreSQL**

The agent refuses the account if:

* it is a superuser;
* it has `INSERT`, `UPDATE`, `DELETE` or `TRUNCATE` on any table outside the system catalogs;
* on PostgreSQL 15 and later, it has `CREATE` on any schema.

PostgreSQL 14 and earlier grant `CREATE` on `public` to every role by default, so the agent skips the `CREATE` check there. Revoke it yourself if you can: `REVOKE CREATE ON SCHEMA public FROM PUBLIC;`

**MariaDB and MySQL**

The agent reads the account's grants, including the grants of every role it holds. It refuses the account if any grant outside the system databases includes `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`, `INDEX`, `REFERENCES`, `TRIGGER`, `CREATE VIEW`, `CREATE ROUTINE`, `ALTER ROUTINE`, `EXECUTE`, `EVENT`, `CREATE TEMPORARY TABLES`, `LOCK TABLES`, `FILE` or `GRANT OPTION`.

Roles count even when they are not active: the account can switch to them with `SET ROLE` at any time.

The agent needs MySQL 8.0 or MariaDB 10.6 at least. Older servers handle roles differently, so the agent cannot prove the account is read-only there.

When the check fails, the agent logs the reason and exits with code `1`:

```text theme={"system"}
ERROR source_client refusing to serve: the database account can write (connection role is a superuser)
```

Fix the grants, then start the agent again. It keeps its enrollment, so you do not need a new token.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.