---
title: "Data"
description: "Browse, edit and query the database of an app, SQLite or Postgres"
order: 6
---

moin reads your app's database live from its server, SQLite or Postgres. You can browse tables, edit a cell, add or delete a row and run SQL. Nothing is copied to moin: every read goes to the server and back.

moin finds the database by itself: every SQLite file on a [volume](/docs/guides/deploy#your-data-survives-every-deploy) of the live release, up to three folders deep, and every service that runs [Postgres](/docs/guides/backups#postgres), named `postgres-<service>`. When there are several, name one with `x-moin.database` in [the repo](/docs/guides/config), or pick one with `--db <path>` or `--db postgres-db` in the CLI.

For Postgres, moin runs `psql` inside your Postgres container over its local socket: no password leaves the container. It shows the tables of the `public` schema. Every value you type travels as a psql variable, never as SQL.

moin edits a row by its rowid on SQLite and by its primary key on Postgres. A table without a primary key has no name for a single row: add rows there, and change them with SQL.

## See the tables

Lists the tables of the database with their row counts.

- Web: the app page, **Data** tab
- CLI: `moin db`
- TUI: opens the web: `w`, then `d`
- API: `GET /api/v1/apps/{app}/databases/tables`
- MCP: `list_tables`

## Browse rows

Shows the rows of one table, newest first.

- Web: the app page, **Data** tab, click a table
- CLI: `moin db rows <table>`
- TUI: opens the web: `w`, then `d`
- API: `GET /api/v1/apps/{app}/databases/rows`
- MCP: `query_database`

A read returns at most 1000 rows. `--limit` in the CLI and `limit` in the API and MCP choose fewer; a limit outside 1 to 1000 is refused.

## See the schema

Shows how a table is built: its `CREATE` statement and its indexes.

- Web: the app page, **Data** tab, a table, **Schema**
- CLI: `moin db schema <table>`
- TUI: opens the web: `w`, then `d`
- API: `GET /api/v1/apps/{app}/databases/tables`
- MCP: `list_tables`

## Run SQL

Runs a query on the live database. A query only reads, unless you say it may change data.

- Web: the app page, **Data** tab, the **SQL** box, **Run**
- CLI: `moin sql "SELECT status, count(*) FROM orders GROUP BY status"`
- TUI: opens the web: `w`, then `d`
- API: `POST /api/v1/apps/{app}/databases/query`
- MCP: `query_database`

## Change data

Edits a cell, adds a row or deletes one. With [backups](/docs/guides/backups) set up, moin takes a snapshot first.

- Web: the app page, **Data** tab: click a cell, type, then `Enter`; **Add row**; or the trash icon on a row
- Web: or tick **Allow changes** in the **SQL** box
- CLI: `moin sql --write "UPDATE settings SET value = 'on' WHERE key = 'maintenance'"`
- TUI: opens the web: `w`, then `d`
- API: `PATCH /api/v1/apps/{app}/databases/rows`
- MCP: `execute_sql`

The API adds a row with `POST` and deletes one with `DELETE` on the same path, and runs SQL that changes data through `/databases/query`. It names a row by `rowid` on SQLite, or by `key`, every column of the primary key, such as `{"id": "42"}`. Agents see `execute_sql` as destructive, so they ask first.

## Reads cannot change anything

A query opens the database read-only; on Postgres it runs in a `READ ONLY` transaction and as one statement. A statement that would change data fails, so a typo cannot delete rows. To change data you say so, as above. Every change runs in one transaction.

More from the CLI:

```bash
moin db                              # tables, rows, migration
moin db rows orders --limit 5        # newest rows first
echo "SELECT * FROM customers" | moin sql
```

## The schema belongs to your code

moin changes data, never the schema. Tables, columns and indexes change through migrations in your repo, which your app runs when it starts: goose, golang-migrate, Prisma, Drizzle or your own. That keeps the database on the server and the code in the repo the same.

moin shows where the migrations are. The data page, `moin db` and `list_tables` name the tool and its newest version when they find a `goose_db_version`, `schema_migrations`, `_prisma_migrations` or `__drizzle_migrations` table.

SQL that creates, alters or drops is still possible for an emergency. The web warns you that the next deploy will not know about it.

## Who may read data

Rows are your users' data, as sensitive as secrets. The API and MCP need a token with write access to read rows or run SQL. A read-only token sees the tables and their row counts.

## Requirements

The app keeps SQLite on a volume, as `/data/app.db` in the compose file moin writes. WAL mode is best: your app and moin then read and write at the same time without waiting. Your app turns it on with `PRAGMA journal_mode=WAL`.

Postgres needs the official image or one built on it (`postgres`, `postgis/postgis`, `pgvector/pgvector`, `timescale/timescaledb`), running.

The data page says the app has no database? Then the live release keeps no SQLite file on a volume and runs no Postgres. Check that the database file sits in a mounted folder, such as `/data`, and deploy again. The agent runs as root; after a change it gives the database files back to their owner, so an app that runs as another user keeps working.

With a bucket set up, every change you make here gets a snapshot first, so one restore undoes it: see [A snapshot before every change](/docs/guides/backups#a-snapshot-before-every-change).
