Edit any row with the admin data suite

The universal CRUD console over every table — and the tables it refuses to let you touch.

Owned by Admin · 10 steps · about 20 minutes

Why this exists

The data suite is a generic console over every table in the database. It is introspected per request, so a table that lands in a migration this afternoon appears in the grid this afternoon with no code written anywhere. That is the point: the platform should never need a bespoke admin screen just to fix one bad row.

Power like that needs limits, and the limits are structural rather than polite. Some tables are read-only in the suite, and there is no admin override — the refusal is a 403, not a confirmation dialog. Three things make a table read-only, and the 403 tells you which one it was, so read it rather than guessing. Its module registered it that way; or the suite detected that it carries a matching pair of append-only triggers; or the table has no single-column primary key, so there is no way to name one row to edit. That third one is not a policy at all — it is the console admitting it cannot address the row. Join tables like tier_zones land there, and nothing is wrong when they do. Detection means the ledger and every audit table are protected automatically, without anyone remembering to register them, and if a trigger did somehow let a write through, the database rejects it and you get a 409 rather than a 500.

Every mutation writes one row to admin_audit_logs with the full before and after snapshots, inside the same transaction as the change — so a rolled-back edit leaves no audit row and an audit row always describes an edit that really happened. That table is itself append-only and read-only in its own grid.

Updates use optimistic concurrency: the form sends the values it believed were current, and if someone else changed the row in the meantime you get 409 stale_row with the current values attached, instead of silently flattening their work.

Before you start

  • An admin session. Anonymous gets 401, everyone else including venue_manager gets 403.
  • A safe table to practise on — the seeded sandbox_parents and sandbox_children exist for exactly this.

Practise with

PersonaEmailPasswordNote
adminadmin@club.test admin123the only persona with any access to the data suite at all

Steps 1–10 — Admin

their manual →
  1. 1
    Start at the admin hub. Every module that wants an admin surface registers a card here rather than editing a shared template.
    /admin adminsuite
    Expected result Cards for the data browser, the audit log, users, groups and zones, QA, the training manual and the debug console.
    Watch out for Debug-only cards disappear when debug endpoints are disabled. If a card you expect is missing, that is usually why.
  2. 2
    Open the table directory. Tables are grouped by the module that owns them.
    /admin/data adminsuite
    Expected result Every table in the database, with row counts.
    Watch out for The list is generated by introspection each time. If a table is missing, it does not exist in this database — that is a schema question, not a permissions one.
  3. 3
    Open the sandbox parents grid and get used to the controls: sort, per-page, a free-text search, column filters, and the deleted selector.
    Expected result A paged grid with the row count and the effective policy for the table.
    Watch out for The deleted selector defaults to include, so soft-deleted rows are visible and look normal. Check the is_deleted column before you conclude a row is live.
  4. 4
    Open one row. Read the three things beneath the form: recent audit for this row, the row guard notice if there is one, and the inbound dependencies.
    /admin/data/{table}/{pk} adminsuite
    Expected result Full values, plus who has touched this row before you.
    Watch out for A row guard is per-row immutability, not per-table — an executed contract is sealed while a draft one is editable. The refusal is 403 row_guard_forbidden and it happens before any SQL runs.
  5. 5
    Change one field and save. The form sends both the new value and the value you were shown.
    /api/admin/data/{table}/rows/{pk} PATCH adminsuite
    Expected result The updated row, an updated_at stamp if the table has one, and one audit row with old and new snapshots.
    Watch out for 409 stale_row means somebody edited the row while you were looking at it; the response carries the current values so you can re-decide. Also note that empty string and NULL are different things here — clearing a box is not the same as setting null.
  6. 6
    Insert a row from the new-row form. It pre-fills a fresh id and the current timestamp for you.
    /admin/data/{table}/new adminsuite
    Expected result 201 and a new row, audited as an insert with no old snapshot.
    Watch out for Read-only tables 403 on this page rather than showing you a form you cannot submit. Coercion errors are specific — invalid_integer, invalid_iso8601, invalid_json — so read the code, not just the red box.
  7. 7
    Now try the opposite: open a protected table and attempt an edit.
    Expected result The grid loads read-only, and any write is refused with 403 table_policy_forbidden.
    Watch out for There is no override flag, no admin escape hatch and no 'are you sure'. Ledger corrections go through the ledger's own adjustment posting; audit corrections do not exist at all.
  8. 8
    Dump the effective policy for every table and read the reasons column.
    Expected result Each table's policy plus why: registered by its module, detected append-only triggers, or a composite primary key.
    Watch out for The mismatches list is the one to care about — it names tables whose registered policy and detected triggers disagree.
  9. 9
    Finish in the audit browser and find your own edits by table, by action or by your admin id.
    /admin/data/audit adminsuite
    Expected result Your insert and update, with full before and after JSON.
    Watch out for There are no mutation verbs on this feed. You can read and filter it; nobody can prune it.
  10. 10
    When you need a real query rather than a grid, use the read-only SQL endpoint.
    /debug/adminsuite/sql POST adminsuite
    Expected result Up to 500 rows from a single SELECT or WITH statement.
    Watch out for It is provably read-only: one statement, checked for write opcodes before it runs, with a hard interrupt. Do not go looking for a write mode — there is not one, and adding one would defeat every guarantee above.