Skip to content

CONCEPT Cited by 1 source

Data–policy separation

What it is

Data–policy separation is the architectural discipline of keeping authorization policy logic completely independent from authorization data loading:

  • Policy author writes "when X, then allow/deny Y" and never touches a database query.
  • Engine operator (backend team) owns how data is fetched — what queries run, in what order, against which replicas, with what caching, with what retries.

The interface between the two layers is a declared data dependency — the policy names the fields it needs (e.g. "team.permission", "file.org_id"), the engine figures out how to materialize them.

Why it matters

  • Independent evolution. A database optimization (e.g. switch to a replica, add a cache, change a query plan) must not accidentally change authorization behavior. A new policy must not accidentally trigger new database load.
  • Performance headroom. Figma reported pre-separation that permissions checks were ~20% of their entire database load — with policy logic entangled with ActiveRecord calls, engineers could not safely optimize the data path without risking policy correctness regressions. See sources/2026-04-21-figma-how-we-built-a-custom-permissions-dsl.
  • Progressive / short-circuit loading (patterns/progressive-data-loading) is only possible when the engine owns data loading — it can partition dependencies into batches and exit early when the policy result is already determinable.
  • Ownership clarity. Two team-scale roles (policy author, platform engineer) with well-defined interfaces. Product teams ship new policies without bothering the data layer.

The declared-dependency interface

Two primitives are typical:

  1. Field references in the policy. The policy language names fields by table.column (Figma) or entity.attribute (Cedar) — never a SQL query or ORM call.
  2. Resource-addressing map — some way for the engine to translate the call-site (resource, user) into the IDs of all rows that might be referenced. Figma names this a context_path ({project: …, team: …, org: …, file: …} for a File + {user: …} for a User, merged with composite-key synthesis for org_user, team_role).

The engine then takes (ExpressionDef, context_path) and does whatever it wants to load data, subject only to the constraint that the evaluator sees consistent {table: {column: value}} maps.

Policy PoC anti-pattern: apply? as a Ruby function

Figma's first PoC had policies declare apply?(resource:, **_) as arbitrary Ruby. This broke separation:

  • Side effects possible. Policy could make network calls, write logs, mutate state — no contract that apply? is pure.
  • No dependency analysis. Can't statically extract what the policy reads; must execute it to find out.
  • Couples evaluation to one runtime. Cross-platform requires AST parsing or separate implementations.

The JSON-serializable ExpressionDef pivot closed all three gaps simultaneously.

Relationship to policy-as-data

concepts/policy-as-data stores the policy definitions separately from code. Data–policy separation goes one step further: the policy language itself must not permit data-fetching code. Policy is only pure boolean logic over declared inputs.

Caveats

  • Not zero cost. The policy author loses some control over query shape — if an expensive join is really only needed for one rare policy, the engine still has to load it generically.
  • Requires a context_path-style resource-addressing abstraction. New resource types require registration.
  • Progressive loading (patterns/progressive-data-loading) is an optimization enabled by this separation but not forced by it.

Seen in

Last updated · 200 distilled / 1,178 read