From 205aecc06b813b7434431941573016798ea6df84 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:18:47 +0000 Subject: [PATCH 1/4] docs(provincesystem): describe the database studio Co-Authored-By: Claude Opus 5.5 --- projects/ProvinceSystem/README.md | 1 + .../docs/identity/auth-security.md | 3 +- .../docs/ops/database-studio.md | 70 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 projects/ProvinceSystem/docs/ops/database-studio.md diff --git a/projects/ProvinceSystem/README.md b/projects/ProvinceSystem/README.md index 5db572e..242b0d5 100644 --- a/projects/ProvinceSystem/README.md +++ b/projects/ProvinceSystem/README.md @@ -16,6 +16,7 @@ TFMC runs Minecraft **1.21.10**. See the [shared platform and build baseline](.. - [LuckPerms staff panel and Commands tab](docs/integrations/luckperms.md) - [Rail network](docs/integrations/rail.md) - [Server plugin versions and restart controls](docs/integrations/servers.md) +- [Database studio](docs/ops/database-studio.md) - [Patreon integration](docs/integrations/patreon.md) and [backend protocol](docs/integrations/patreon-backend.md) - [Kit default skins](backend/assets/kit_skins/README.md) - [Map regeneration benchmarks](backend/benchmarks/regen/README.md) diff --git a/projects/ProvinceSystem/docs/identity/auth-security.md b/projects/ProvinceSystem/docs/identity/auth-security.md index 7ac4e37..b5c0d1b 100644 --- a/projects/ProvinceSystem/docs/identity/auth-security.md +++ b/projects/ProvinceSystem/docs/identity/auth-security.md @@ -249,7 +249,8 @@ elsewhere: the Discord session cookie, a website role with the capability, and t site `Origin` on writes. It returns **401** when signed out and **403** without the capability. Used by `POST /skins/codes/inspect` (`inspect_codes`) and the precedent routes when no `X-Staff-Key` is sent (`use_precedent`), and the diagnostic report -routes (`view_diagnostics`, see [ops/diagnostic.md](../ops/diagnostic.md)). +routes (`view_diagnostics`, see [ops/diagnostic.md](../ops/diagnostic.md)), and the +database studio (`use_database`, root only, see [ops/database-studio.md](../ops/database-studio.md)). ### UI dev bypass (non-production only) diff --git a/projects/ProvinceSystem/docs/ops/database-studio.md b/projects/ProvinceSystem/docs/ops/database-studio.md new file mode 100644 index 0000000..1ae6f4d --- /dev/null +++ b/projects/ProvinceSystem/docs/ops/database-studio.md @@ -0,0 +1,70 @@ +# Database studio + +Owners (website role `root`) browse, query and edit the site's databases at +**Staff panel → Database** (`/admin/database`). The tab only shows for roles with +the `use_database` capability, which is root only. Code: +[`backend/src/dbstudio/studio.py`](https://github.com/TF-Minecraft/ProvinceSystem/blob/main/backend/src/dbstudio/studio.py), +[`backend/src/api/database_routes.py`](https://github.com/TF-Minecraft/ProvinceSystem/blob/main/backend/src/api/database_routes.py) +and `frontend/app/components/admin/database/`. + +## Databases + +Each site lists the databases it can reach: + +| Id | What | Access | +|----|------|--------| +| `site` | `province.db`: accounts, characters, wardrobes, chronicle and ledger indexes, the staff audit log | Read and edit | +| `coreprotect` | The CoreProtect SQLite file named by `COREPROTECT_DB` (Main on www, Dev on the Dev site) | Read only | +| `supabase` | The Postgres database in `SUPABASE_DB_URL` (Precedent and patch notes), `public` schema; www only | Read and edit | + +The list shows each database's tables, size and how many audited queries it has had. +Opening one shows its tables with columns and approximate row counts (`max(rowid)` on +SQLite, `reltuples` on Postgres), query tabs and table tabs. Query tabs are kept in +the browser's local storage per database. + +## Reading + +Reads run on connections that cannot write: SQLite opened with `mode=ro` and +`PRAGMA query_only`, Postgres in a read-only transaction. Each statement returns at +most 1,000 rows and stops after 10 seconds on the site and Postgres databases. +SQLite runs one statement per query. + +CoreProtect reads go through the same guarded reader as the CoreProtect pages +([coreprotect.md](../integrations/coreprotect.md)): one reader at a time and a +3-second budget, because a long read holds off CoreProtect's writes. Filter on +indexed columns (`user`, `time`, `wid`, `x`/`z`) and add a `LIMIT`. + +A table tab pages through `SELECT * FROM ` with an optional `WHERE` filter +and sorting by any column; **Open as query** copies the statement into a query tab. + +Text values in columns whose names look secret (`token`, `secret`, `password`, +`nonce`, `api_key`, such as `token_hash` and `refresh_token`) show as `••••••`. +This keeps them off the screen; it is not access control. Integers beyond 2^53 are +sent as text, and blobs as their size and first 32 bytes in hex. + +## Editing + +On a writable database, switch a query tab from **Read** to **Edit**. The studio +shows the statement and asks before running it. Edits run in one transaction with +foreign keys on (SQLite) and the same time limits, and the result shows the rows +changed. The audit log table `admin_audit` cannot be changed, dropped or have its +triggers changed from the studio, and SQLite `ATTACH` is refused in both modes. + +## Audit + +Every browse, query and edit is written to the staff audit log as +`database.read` or `database.write`, with the database, the statement (first 2,000 +characters), the outcome and the row counts. Schema loads are not audited. + +## Routes + +| Route | Method | Purpose | +|-------|--------|---------| +| `/admin/database` | GET | Databases with tables, size and query counts | +| `/admin/database/{id}/schema` | GET | Tables, views and columns | +| `/admin/database/{id}/browse` | POST | One page of a table: `table`, `offset`, `limit` (≤ 500), `order`, `descending`, `where` | +| `/admin/database/{id}/query` | POST | Run `sql`; `write: true` for an edit | + +All need a root session; the POST routes also need the site's `Origin`. SQL errors +come back as **400** with the database's message, timeouts as **408**, a busy +CoreProtect reader as **409** and refused statements as **403**. From 57fc7ae65f87984d3b43df0ae772b23f1a96ab4f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:36:57 +0000 Subject: [PATCH 2/4] docs(provincesystem): describe the studio's statement rules and preview tabs Co-Authored-By: Claude Opus 5.5 --- .../docs/ops/database-studio.md | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/projects/ProvinceSystem/docs/ops/database-studio.md b/projects/ProvinceSystem/docs/ops/database-studio.md index 1ae6f4d..739e846 100644 --- a/projects/ProvinceSystem/docs/ops/database-studio.md +++ b/projects/ProvinceSystem/docs/ops/database-studio.md @@ -24,10 +24,17 @@ the browser's local storage per database. ## Reading +Every query is one statement. Before connecting, the studio refuses transaction and +session control (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `SET`, `RESET`, `DISCARD`, +…), `VACUUM`, `ATTACH`, `DETACH` and `COPY`; semicolons inside strings, quoted names, +dollar quotes and comments are ignored when counting statements. + Reads run on connections that cannot write: SQLite opened with `mode=ro` and -`PRAGMA query_only`, Postgres in a read-only transaction. Each statement returns at -most 1,000 rows and stops after 10 seconds on the site and Postgres databases. -SQLite runs one statement per query. +`PRAGMA query_only`, Postgres in a read-only transaction through a server-side cursor, +so a Postgres read must be a query (`SELECT`, `WITH`, `VALUES`, `TABLE`) and its rows +arrive as they are fetched. Each statement returns at most 1,000 rows and stops after +10 seconds on the site and Postgres databases. SQLite reads may call schema pragmas +such as `table_info`; pragmas that change settings are refused. CoreProtect reads go through the same guarded reader as the CoreProtect pages ([coreprotect.md](../integrations/coreprotect.md)): one reader at a time and a @@ -36,6 +43,11 @@ indexed columns (`user`, `time`, `wid`, `x`/`z`) and add a `LIMIT`. A table tab pages through `SELECT * FROM
` with an optional `WHERE` filter and sorting by any column; **Open as query** copies the statement into a query tab. +One click on a table opens it in a reused preview tab (italic title); a double click, +or paging, sorting or filtering in it, keeps the tab open. + +CSV exports prefix text that starts with `=`, `+`, `-`, `@`, a tab or a carriage +return with `'`, so player-written text cannot run as a spreadsheet formula. Text values in columns whose names look secret (`token`, `secret`, `password`, `nonce`, `api_key`, such as `token_hash` and `refresh_token`) show as `••••••`. @@ -48,7 +60,9 @@ On a writable database, switch a query tab from **Read** to **Edit**. The studio shows the statement and asks before running it. Edits run in one transaction with foreign keys on (SQLite) and the same time limits, and the result shows the rows changed. The audit log table `admin_audit` cannot be changed, dropped or have its -triggers changed from the studio, and SQLite `ATTACH` is refused in both modes. +triggers changed from the studio. Triggers cannot be created on the site database at +all, because a stored trigger would later run outside the studio's checks, and +`PRAGMA writable_schema` and other setting changes are refused. ## Audit From bb99b908b119266a0e235740fdae16b643f6a3c4 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:50:36 +0000 Subject: [PATCH 3/4] docs(provincesystem): single statements enforced by the databases, audited edits and schema loads Co-Authored-By: Claude Opus 5.5 --- .../ProvinceSystem/docs/ops/database-studio.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/projects/ProvinceSystem/docs/ops/database-studio.md b/projects/ProvinceSystem/docs/ops/database-studio.md index 739e846..b01f1d8 100644 --- a/projects/ProvinceSystem/docs/ops/database-studio.md +++ b/projects/ProvinceSystem/docs/ops/database-studio.md @@ -24,13 +24,16 @@ the browser's local storage per database. ## Reading -Every query is one statement. Before connecting, the studio refuses transaction and -session control (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `SET`, `RESET`, `DISCARD`, +Every query is one statement, enforced by the databases themselves: Python's +`sqlite3` refuses a second statement, and Postgres statements go through the extended +protocol (psycopg 3), which refuses several commands. As a second layer with clearer +messages, the studio also refuses before connecting transaction and session control (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `SET`, `RESET`, `DISCARD`, …), `VACUUM`, `ATTACH`, `DETACH` and `COPY`; semicolons inside strings, quoted names, dollar quotes and comments are ignored when counting statements. Reads run on connections that cannot write: SQLite opened with `mode=ro` and -`PRAGMA query_only`, Postgres in a read-only transaction through a server-side cursor, +`PRAGMA query_only`, Postgres in a read-only transaction through a server-side cursor +(`DECLARE … CURSOR FOR`), so a Postgres read must be a query (`SELECT`, `WITH`, `VALUES`, `TABLE`) and its rows arrive as they are fetched. Each statement returns at most 1,000 rows and stops after 10 seconds on the site and Postgres databases. SQLite reads may call schema pragmas @@ -60,7 +63,7 @@ On a writable database, switch a query tab from **Read** to **Edit**. The studio shows the statement and asks before running it. Edits run in one transaction with foreign keys on (SQLite) and the same time limits, and the result shows the rows changed. The audit log table `admin_audit` cannot be changed, dropped or have its -triggers changed from the studio. Triggers cannot be created on the site database at +triggers or indexes changed from the studio. Triggers cannot be created on the site database at all, because a stored trigger would later run outside the studio's checks, and `PRAGMA writable_schema` and other setting changes are refused. @@ -68,7 +71,10 @@ all, because a stored trigger would later run outside the studio's checks, and Every browse, query and edit is written to the staff audit log as `database.read` or `database.write`, with the database, the statement (first 2,000 -characters), the outcome and the row counts. Schema loads are not audited. +characters), the outcome and the row counts. An edit is logged as `requested` before +it runs, so if the audit log cannot be written the edit does not happen. Opening the +list (`database.list`) and a database's tables (`database.schema`) are logged too. +The list's query count covers reads and edits with an outcome. ## Routes From 148ece0f7abcc8249eba7b1b33cb6675bc747388 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:00:11 +0000 Subject: [PATCH 4/4] docs(provincesystem): note the studio's sqlite_sequence guard Co-Authored-By: Claude Opus 5.5 --- projects/ProvinceSystem/docs/ops/database-studio.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/ProvinceSystem/docs/ops/database-studio.md b/projects/ProvinceSystem/docs/ops/database-studio.md index b01f1d8..4540d43 100644 --- a/projects/ProvinceSystem/docs/ops/database-studio.md +++ b/projects/ProvinceSystem/docs/ops/database-studio.md @@ -63,7 +63,8 @@ On a writable database, switch a query tab from **Read** to **Edit**. The studio shows the statement and asks before running it. Edits run in one transaction with foreign keys on (SQLite) and the same time limits, and the result shows the rows changed. The audit log table `admin_audit` cannot be changed, dropped or have its -triggers or indexes changed from the studio. Triggers cannot be created on the site database at +triggers or indexes changed from the studio, and `sqlite_sequence` (which holds its +ID counter) cannot be written. Triggers cannot be created on the site database at all, because a stored trigger would later run outside the studio's checks, and `PRAGMA writable_schema` and other setting changes are refused.