Access control, explained like you're new to it
Relata checks every request against a permission list before any data leaves the database. You can't accidentally skip it — and that includes requests that arrive through PostgreSQL, S3, or MongoDB clients rather than the normal API.
This page is the practical, copy-paste guide. It assumes no security background. If you want the design reasoning instead, see Governance.
The 60-second version
- Every connection arrives through a door (the HTTP API, the PostgreSQL wire door, the S3 door, …). Each door has its own built-in role, e.g.
http-client,pgwire-client,s3-client. - When a request comes in, Relata checks the role's permissions for the type (think: table) being touched — read or write.
- Deny always wins. If anything says no, the answer is no.
- You extend permissions with one environment variable (
RELATA_ACL_GRANT) and hide columns with another one (RELATA_CELL_POLICIES). Both are read once at startup.
"I registered my own type and now I get 403"
The most common first stumble. Built-in types (Person, Transaction, …) are pre-granted. Types you create yourself are not — until you grant them.
Add one environment variable and restart the server:
# Grant read+write on your custom types to every protocol door
RELATA_ACL_GRANT="Customer:read+write,Order:read+write" relata serveThat's it. The same grant automatically covers the internal system role (read only) so background features like materialized views keep working.
Shortcuts that save time:
RELATA_ACL_GRANT="Customer"(no:perms) means read only — the safe default when handing a type to an analytics tool.- Multiple types are one comma-separated list;
+joins permissions:read+write. - The grant also registers the type, so a brand-new server won't return "unknown type" before your first write.
"I want to hide a column from everyone" (SSN, IBAN, salaries…)
Use the cell-policies variable. It takes a comma-separated list of Type.column=action entries:
RELATA_CELL_POLICIES="Person.ssn=mask,BankAccount.iban=redact" relata serveActions:
| Action | What the caller sees |
|---|---|
mask or redact | [REDACTED] — the row still comes back, just with that cell blanked |
tokenize | A fake-but-stable stand-in value (see next section) |
allow | Nothing happens (explicit "leave this alone") |
A query result then looks like this — the row is not dropped, only the cell is masked:
{"name": "Alice", "ssn": "[REDACTED]", "age": 30}RELATA_CELL_POLICIES value makes the server refuse to start, with an error naming the bad entry. That's deliberate: a silently-ignored typo would mean you think a column is protected when it isn't. The format is strict — Type.column=action, not a file path or JSON."I want to hide a column but still join/count on it"
Masking destroys the value. Tokenizing replaces it with a stable fake: the same input always yields the same token, so equality checks, grouping, and dedup still work — but nobody can read the original.
RELATA_CELL_POLICIES="Person.ssn=tokenize"
RELATA_TOKENIZE_KEY="<your-32-byte-hex-key>" relata servetokenize without setting RELATA_TOKENIZE_KEY also refuses to start — a protected field is never silently left raw because of a config gap. Keep the key safe: it is what maps tokens back to values, and losing it makes tokenized columns permanently opaque."Why am I getting 403 / Forbidden?"
Work down this checklist — it covers essentially every case:
- Is it an admin-only endpoint? Registering or deleting types (
POST /types,DELETE /types/{name}) needs the admin token, not the normal bearer token. Onserver/clusterprofiles the admin token isRELATA_ADMIN_TOKEN. - Is it your own type? Custom types need
RELATA_ACL_GRANT(see above). Symptom: built-in types query fine, yours 403s. - Did the env var actually load? All of these are read at startup — restart, and watch the first seconds of logs: each grant logs
RELATA_ACL_GRANT: granted access. - Still stuck? Ask the database to explain the decision:
EXPLAIN POLICY SELECT name, ssn FROM Person LIMIT 5This returns which rules matched, in what order, and what each did (allow / deny / mask) — it's the single best debugging tool for access questions.
SourceTrueIdentity) are blocked at egress unconditionally, whatever your grants say. That's by design — see Governance."An auditor asked: who looked at this data?"
Two pieces, both already running:
1. Purpose tags. Queries may declare why they're reading. It's optional and per query:
PURPOSE 'analytics' SELECT name, revenue FROM Account LIMIT 1002. The audit trail. Every query and write lands in a tamper-evident, hash-chained log. Page through it with filters:
# Everything for one purpose
curl 'http://localhost:9090/audit/entries?purpose=analytics&limit=50' \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN"
# Everything touching one type, newest first
curl 'http://localhost:9090/audit/entries?type=Person&limit=50' \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN"Useful filters: purpose, principal (who), type, since/until (timestamps), outcome=ok|error. /audit/count gives totals; /audit/proof returns a cryptographic inclusion proof that an entry is really in the chain.
RELATA_PURPOSE_MODE=strict and list the allowed ones in RELATA_PURPOSES=analytics,audit,compliance. Queries with an unlisted purpose are then rejected before they run.What you can't do (yet) — so you don't lose an afternoon
- Per-door grants via env vars.
RELATA_ACL_GRANTreaches every door. You cannot say "read-only for S3, read-write for HTTP" with today's env-var surface. - Cedar policy files. The engine has Cedar-inspired internals, but you cannot load a Cedar policy document on a running server today. If you've seen
permit(...)/forbid(...)examples elsewhere, they don't apply to the current release.
Both are tracked; until they ship, the honest answer is: type-level grants + column masking (this page) are today's operator toolbox.
See also
- Per-Door ACL — what a "door principal" is and why audit rows are attributable to a protocol
- Auth & Security — tokens, TLS, and the admin surface
- Environment Variables — the full
RELATA_*reference - Governance — the design behind all of this