Access Rules Across Joins
Each lens enforces its own rules. When you include a relation, the
target lens's rule is checked — independently — against the caller's
identity. Rule composition is always most-restrictive wins.
The flow
If either rule denies, the result differs:
- Primary denied → the whole request returns
403. No data, nothing joined. - Include denied → primary rows return normally; the failed relation comes back empty;
meta.includeErrorsgets one entry.
Most-restrictive wins
Per-row, if a rule function returns a filter (e.g. row-level security), the planner intersects it with the FK filter it was already going to apply:
When the caller includes reviews, the planner sends:
The intersection is computed once and passed to batchRead — you don't
lose performance to this composition.
Fail-partial
A denied include doesn't blow up the whole response. Semantics:
This is deliberate: if a visitor is allowed to see recipes but not reviews, they still get a working recipes page — the reviews panel is empty with a clear error surface the UI can handle.
sk_ keys bypass
Secret keys bypass access rules — they're treated as server-trusted
credentials. Use them on backends, never expose them client-side. On
sk_ keys:
- All
grants.*checks are skipped. - Every declared relation is includable.
- No
meta.includeErrorsfor access reasons (capability / source errors still show up).
End-user JWT (X-User-Token)
For pk_ keys, pass the end user's identity in X-User-Token (or
beam.withUser(token) on the client). Rules that are functions receive
those claims:
The same claims object is passed to every lens's rule function along the join plan — a moderator sees moderator-scoped data through every relation in one consistent query.
Debugging
- Unexpected empty relations? Check
meta.includeErrors— ifreasonisaccess_denied, the target lens's rule rejected the caller. - Unexpectedly populated relations? You're probably hitting the endpoint with an
sk_key and forgot to pass anX-User-Token.sk_bypasses everything. - Function rules misbehaving? Log the result of the rule function against a sample claims object — the planner just applies whatever the function returns.
The audit trail
Every join check is logged internally at DEBUG level with the resolved
filter (if any) and the denial reason. Platform admins can read these
in the audit log for any request. See
Auth & RBAC for the full rule vocabulary.
Next: Bridges Without Support.