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
ActiveRecordcalls, 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:
- 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. - 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 acontext_path({project: …, team: …, org: …, file: …}for aFile+{user: …}for aUser, merged with composite-key synthesis fororg_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¶
- sources/2026-04-21-figma-how-we-built-a-custom-permissions-dsl
— canonical instance.
ApplyEvaluator+DatabaseLoadernamed explicitly as the two halves;context_pathas the resource-addressing primitive; ~20% database-load share pre-DSL as the quantified pain.