Platform

Row-level security

Policies that bind a logical attribute to a value set for a set of subjects, injected as a filter into the query plan below any aggregation: decided per query, cached per decision, and fail-closed

Decision and enforcement are separate

The design follows the standard split, which is what keeps the rule out of the client:

  • The decision point answers, for a subject and a set of resources: denied, these row filters, these column masks.
  • The enforcement point sits between compiling a query and executing it. It asks for the decision, and injects it into the plan.

Nothing downstream can opt out, because by the time a query executes the restriction is part of it.

How a policy is expressed

A policy binds an attribute - a logical name mapped to a physical column - to a value set, for a set of subjects.

Value sets are reusable allow-lists, and an entry can be:

  • a static literal,
  • a claim from the caller's token, resolved at decision time,
  • or a per-subject value loaded in advance.

The middle one is what makes a single policy serve an organisation: "the region on the caller's token" is one policy, not one policy per region.

How policies combine

Situation Result
Several policies on the same attribute Their values are OR-unioned
Policies on different attributes AND-combined, as nested filters
An empty allowed set, with nulls not permitted No rows, explicitly and not by accident

Where the filter lands in the plan

Strictly below any aggregation. That is the detail that decides whether row security is real: filtering after a sum gives a correct-looking total computed over rows the caller may not see. Here the restriction applies before the aggregate, so a total is the total of what this person may see.

Fail-closed

If the decision cannot be reached, the request errors out. It does not log and continue, and it does not fall back to unrestricted.

Calls arriving without a subject - a service token, a trusted internal path - skip row security by construction, and the platform records that as a bypass event instead of leaving it indistinguishable from a normal query.

Freshness

Decisions are cached to keep repeated queries cheap, and every write to a policy bumps a version and invalidates the cached decisions that depended on it. A permission change does not wait for a cache to expire.

Auditability

Every decision, and every bypass, is written to a decision log, queryable and administrator-gated - so "why did this person see this row" and "what ran without row security" are both answerable after the fact. The platform-wide record is separate - see Audit.

  • Column masking - the second half of the decision: which values inside the rows a caller may read.
  • Roles and grants - whether the object is visible at all, decided before any of this.
  • Data engine - where the plan these filters are injected into is compiled and executed.
  • Tenant isolation - the layer underneath, answering which organisation a query is even looking at.