Skip to content

Secrets and hidden columns

What a browser may never have must be kept from the App by the database or the server; what an App leaves off a page is still served.

-- PostgreSQL: the App's role reads staff without its password hash
REVOKE SELECT ON staff FROM tablewalk_reader;
GRANT SELECT (id, name, warehouse_id, phone) ON staff TO tablewalk_reader;
Way Without sign-in What it does
A database role without SELECT on the column (PostgreSQL, MySQL) Holds The column never reaches tablewalk. The strongest answer.
A view that omits it, with the table out of reach: a role without SELECT on the table, or a PostgreSQL connection’s "schemas" list (--schema) naming only the views’ schema Holds A view alone hides nothing while its table is still readable on the connection.
A separate SQLite file Holds SQLite has no roles: every table in the file is readable on the connection, so a column to keep out lives in another file.
Leaving the table out of resources Holds An App serves only its declared resources: the table is not there for its readers, signed in or not. The standalone browser still reads it.
hidden: ['password_hash'] on the resource Holds Cut from what the server answers the App’s readers, signed in or not; a filter or sort naming it is refused, raw SQL is refused, and history redacts it. Never the primary key, the address or a search field.
"hidden": { "staff": ["password_hash"], "*": ["*_token"] } on the connection in tablewalk.json Holds The same cut for the database browser, MCP and every App on the connection; raw SQL is refused there, and a delete on a table with a hidden column.
fields: { read: [...] } on the resource Refused With auth and auth.roles: a column ceiling; left out of metadata and results, and a query naming it is refused.
display: { token: { format: 'masked' } }, or masked: { prefix: 'ENC:' } Hides nothing Draws a placeholder for the value, the record’s name included; the value still reaches the browser.
show, a page’s fields, nav Hides nothing Only decides what of a declared resource is drawn (what an App serves).
"rows": false on the connection Holds No row values at all: shape, counts and plans only. For an agent’s connection, not an App’s.

audit: { redact: ['ssn'] } is not a way to hide a column: it still reads. It keeps the value out of the write trails, which record a keyed hash of it, and out of a page’s history(), which says only that it changed.

Connection URLs and service tokens live in tablewalk.json as ${VARIABLE} references, read from the server’s environment; an unset one is an error. Sign-in reads BETTER_AUTH_SECRET from the environment. None of them reaches an App’s definition or a browser.

app.ts is evaluated in a worker with no environment, because whatever the definition builds from a value (a title, a link, a query) is part of the App every browser is sent. tablewalk.json passes the names it lists:

{ "apps": [{ "dir": "apps/ops", "env": ["REGION", { "name": "MAP_KEY", "secret": true }] }] }
  • A name that reads like a credential (PASS, SECRET, TOKEN, KEY, DATABASE_URL, PG…) is refused unless it is written { "name", "secret": true }.
  • Each name, never a value, is a line of authority.lock (env REGION), so passing one more is a reviewed change.
  • Reading a name the file does not pass throws, naming it; 'REGION' in process.env asks without throwing.
  • tablewalk check --app <dir> and tablewalk build --config read the same list from a tablewalk.json that lists that App (and no other).
  • A change to the list needs a restart (reload and restart).

Server-only modules (--commands, --jobs, --policy) run in the server itself, with its environment, and never reach the browser.