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 hashREVOKE SELECT ON staff FROM tablewalk_reader;GRANT SELECT (id, name, warehouse_id, phone) ON staff TO tablewalk_reader;Keeping a column out
Section titled “Keeping a column out”| 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.
Credentials in configuration
Section titled “Credentials in configuration”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.
The environment of an App’s definition
Section titled “The environment of an App’s definition”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.envasks without throwing. tablewalk check --app <dir>andtablewalk build --configread 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.