Permissions
A schema file alone serves everything to everyone holding the token. The optional second file narrows that:
n6kd -f schema.sql --perms perms.yml
The rules are declarative, they live outside the application, and they are checked when the process starts rather than when the first client shows up.
The file
version: 1
source:
catalog: memory
schema: main
catalogs:
demo:
schema: main
tables:
customers: read
orders: readwrite
audit_log: read
demo_readonly:
schema: main
tables:
customers: read
orders: read
audit_log: read
source names where the real tables live. Each entry under catalogs is a
view of that source that clients can attach by name, and each names its
tables explicitly with read or readwrite. A table not listed is not served —
absence is denial, so a new table added to the schema is invisible until someone
grants it.
Two catalogs, one copy of the data
demo and demo_readonly above are the same physical rows with different
rules. There is no replica and no second process. Attach both from one client
and the difference shows up per statement:
ATTACH 'n6ks://…' AS demo (TYPE n6k);
ATTACH 'n6ks://…' AS demo_readonly (TYPE n6k);
UPDATE demo.orders SET status = 'refunded' WHERE id = 42; -- ok
UPDATE demo_readonly.orders SET status = 'refunded' WHERE id = 42;
Binder Error: Table 'orders' is read-only
That is one server handing an analyst a read-only endpoint and an application a writable one, over identical data.
Checked at boot
Every rule is verified against the real schema before the listener opens:
INFO catalog "demo" serves 3 tables from memory.main
INFO demo.main.audit_log read
INFO demo.main.customers read
INFO demo.main.orders readwrite, primary key id (from schema)
INFO perms check passed: 6 tables found in memory.main,
1 writable table with a usable primary key
Three classes of mistake are caught here rather than in production:
- a table named in
perms.ymlthat does not exist in the schema - a
readwritetable with no discoverable primary key — writes need one to address rows, so it is refused up front instead of failing on the firstUPDATE - an unknown key in the YAML, which is a parse error rather than a silently ignored line. A typo in a permission name cannot quietly grant nothing.
A bad permissions file fails the boot. A deploy either serves the policy you wrote or does not serve at all.
The layers
Three separate things decide whether a statement runs, and it is worth keeping them apart:
| Layer | Question | Where it lives |
|---|---|---|
| Auth | Who is connecting? | A token, checked at the upgrade |
| Permissions | What may they touch? | perms.yml, checked at boot |
| Lockdown | Can their SQL leave the database? | On by default |
They compose but do not substitute. A valid token still cannot write to a
read table; a readwrite grant still cannot read /etc/passwd.
Limits
Permissions are per table, not per row or per column — there is no predicate
that scopes a reader to their own tenant. When you need that, keep the
narrowing in the source: define a view in schema.sql and grant the view, or
scope the connection upstream the way the
Postgres page does.
Grants are also static for the life of the process. Changing perms.yml
means a restart, which is a fair trade for the boot-time verification but does
mean permissions are a deploy, not an API call.