Access control that explains itself, with ReasoningLayer
It's Monday. Alice joins the Engineering team.
Nobody opens the permissions table and types a row granting Alice access to design.pdf. They add her to one group — Engineering — and move on. The group already has the file. That's the whole change.
On Friday, an auditor asks the question every access system is eventually asked: Can Alice edit design.pdf — and exactly why?
In most systems, "why" is a forensic exercise. You walk her group memberships, expand the roles those groups carry, resolve the permissions those roles imply, and check there's no denial hiding three layers down. The answer is real, but it lives scattered across joins and application code, and reconstructing it by hand is precisely the work nobody has time for during an audit.
It should be one question, with one answer, and the answer should arrive with its own proof.
That's what we built.
See it before you read about it#
Every grant the demo shows you is derived, not stored. Alice's name appears under "can view design.pdf" even though no row in the knowledge base says so, because the engine worked it out from two facts and two rules — and it hands you the chain it followed to get there.
Don't take our word for the derivation — drive it yourself. ▶ Open the live IAM demo and ask it who can touch a file: every answer is a real backward-chaining query — the engine works backward from your question to the facts that settle it — and every row carries the proof that produced it.
Permissions you never wrote#
Here is everything the knowledge base actually stores for our scenario. Two relationships:
- Membership: Alice is a member of the Engineering group.
- One grant: the Engineering group can modify
design.pdf.
That's it. No row says "Alice can modify design.pdf." No row says "Alice can view design.pdf." Neither is stored — both are true, and the engine derives them on demand from two rules that are themselves data — written in the same language as the facts (they're homoiconic):
Rule 1 — modify implies view. If a subject can
modify_filean object, that subject canview_fileit.Rule 2 — group inheritance. If a subject is a member of a group, and the group holds a permission on an object, the member holds that permission too.
Ask "can Alice view design.pdf?" and the engine chains both rules. Rule 2 turns the group's modify grant into Alice's modify grant. Rule 1 turns Alice's modify grant into a view grant. Two hops, no stored permission, a true answer.
This is the spine of the whole approach: policy is data, and grants are derived. Your access rules stop being thousands of pre-computed permission rows that drift out of sync the moment an org chart changes, and become a small set of rules that hold over whatever facts are true right now. Move Alice to a different group and you don't run a migration to recompute her permissions — there were never any rows to recompute. The next question simply gets a different answer.
Every answer comes with a proof#
A permission check that returns true tells you what. It doesn't tell you why, and during an audit, an incident, or a "wait, how does the intern have production access?" moment, why is the only thing that matters.
Because grants are derived by rules rather than read from a table, the derivation is the answer. When the engine confirms Alice can view design.pdf, it returns the tree it walked: group membership → group's modify grant → Alice's modify grant (Rule 2) → Alice's view grant (Rule 1). Two rule applications, and you can read every one of them.
Don't take that on faith — here's the actual shape the engine returns for "can Alice view design.pdf?":
{
"allowed": true,
"certainty": 1.0,
"query_time_ms": 1,
"proof": {
"goal_display": "iam_permission(action: \"view_file\", object: \"design.pdf\", subject: \"alice\")",
"rule_label": "iam_permission(action: \"view_file\", object: ?O, subject: ?S)",
"subproofs": [
{
"goal_display": "iam_permission(action: \"modify_file\", object: \"design.pdf\", subject: \"alice\")",
"rule_label": "iam_permission(action: ?A, object: ?O, subject: ?U)",
"subproofs": [
{ "goal_display": "iam_membership(group: \"engineering\", member: \"alice\")" },
{ "goal_display": "iam_permission(action: \"modify_file\", object: \"design.pdf\", subject: \"engineering\")" }
]
}
]
}
}Read it top-down and it is the derivation, in full. Each node's goal_display is the engine's own rendering of the conclusion proven there — ground, subject: "alice" and all. Each rule_label names the rule that produced it, and a node's subproofs are what that rule stood on:
- The root proves Alice can view
design.pdf, by Rule 1 (view_filefollows frommodify_file). - That rests on Alice's modify grant — itself derived, by Rule 2 (group inheritance), from two things the knowledge base actually stores: her
iam_membershipin Engineering, and the Engineering group'smodify_filegrant ondesign.pdf. Both are facts; the recursion bottoms out there.
Two rule applications, two stored facts, every node rendered ready to read. Nothing in the front end assembled any of it — the engine emits the whole tree alongside the verdict. (Simplified for the page: the engine wraps this in a solutions[] array — allowed is just solutions.length > 0 — and every node also carries its term ids and the full variable substitution, trimmed here to keep the chain legible.)
That changes what an access decision is. It's no longer an opaque boolean you have to trust and then reverse-engineer when someone challenges it. It's a claim with a citation — the tree above. "Yes, and here is the chain" — read straight from the engine's own derivation by the same query that answered the question, not reverse-engineered by hand afterwards. Explainability isn't a feature you bolt on; it falls out of deriving the answer in the first place.
And the negative case is just as clean. Ask "can Bob modify secrets.txt?" when Bob only holds a view grant, and there is no derivation to be found — Rule 1 runs one direction only, view never implies modify. No proof, no access. The absence of a chain is itself the audit record.
One query, every kind of subject#
Roles and groups and users and service accounts are all things that can hold a permission. In ReasoningLayer they share a supertype — subject — with user and user_group as subsorts beneath it. That hierarchy isn't decoration; it changes what a single query returns.
Ask the engine for every subject and it answers with users and groups in one shot:
MATCH iam_subject(name: ?N);You queried the supertype once; you got every subtype back. For access control this is the difference between "list everyone and everything that could touch this resource" being one question versus a UNION across every table you happen to have modelled subjects in. Add a new kind of subject tomorrow — a team, a bot, a third-party app — make it a subsort of subject, and the same query picks it up with no rewrite. The type lattice does the work that enumeration used to.
The whole thing, in code#
The model above is the entire setup. You assert the facts, add the two rules, and ask. Here's the shape of it against the TypeScript SDK — (not wiring this up yourself? Skip the block; the one line to keep is right after it.)
import { getClient } from './sdk-client';
const client = getClient(); // your workspace tenant
// The only things actually stored: a membership and one grant.
await client.inference.bulkAddFacts({
facts: [
{ sortName: 'iam_membership', features: { member: 'alice', group: 'engineering' } },
{
sortName: 'iam_permission',
features: { subject: 'engineering', object: 'design.pdf', action: 'modify_file' },
},
],
});
// Rule 1 — a modify permission implies a view permission.
await client.inference.addRule({
term: {
sortName: 'iam_permission',
features: { subject: { name: '?S' }, object: { name: '?O' }, action: 'view_file' },
},
antecedents: [
{
sortName: 'iam_permission',
features: { subject: { name: '?S' }, object: { name: '?O' }, action: 'modify_file' },
},
],
certainty: 1.0,
});
// Rule 2 — a member inherits the group's permissions.
await client.inference.addRule({
term: {
sortName: 'iam_permission',
features: { subject: { name: '?U' }, object: { name: '?O' }, action: { name: '?A' } },
},
antecedents: [
{ sortName: 'iam_membership', features: { member: { name: '?U' }, group: { name: '?G' } } },
{
sortName: 'iam_permission',
features: { subject: { name: '?G' }, object: { name: '?O' }, action: { name: '?A' } },
},
],
certainty: 1.0,
});
// "Can Alice view design.pdf?" — and bring back the proof.
const { solutions } = await client.inference.backwardChain({
goal: {
sortName: 'iam_permission',
features: { subject: 'alice', object: 'design.pdf', action: 'view_file' },
},
maxSolutions: 5,
maxDepth: 50,
});
const allowed = solutions.length > 0; // → true, with solutions[0].proof spelling out whyThe takeaway, if you skipped the block: the rules are values you hand the engine, the answer comes back with its derivation, and adding a new policy is adding a rule — not editing the code that reads permissions.
Where this goes: denials, clearance, and scale#
The Engineering-file scenario is small on purpose — it makes the mechanics visible. The same machinery runs enterprise access control unchanged; only the data grows. A few things you reach for the moment the problem gets real:
Closed-world denials, with Negation as Failure. "Has the permission" is rarely enough. Real access is granted only if the subject holds the permission and is not blacklisted, not suspended, not contract-expired. NAF lets you express exactly that: a check that succeeds only when those denial facts fail to prove. Suspend an account and every derived grant it had evaporates on the next query — you revoke by stating one fact, not by hunting down every permission the suspension should cancel.
Clearance as a lattice, not a column. Model clearance levels as nested sorts — Clearance5 ⊂ Clearance4 ⊂ Clearance3 ⊂ Clearance2 ⊂ Clearance1 — and "a Clearance-3 holder satisfies a Clearance-2 requirement" stops being an if level >= required scattered through your codebase. It becomes a fact about the type hierarchy that every query respects for free. The same shape models the resource side: a service that requires Clearance-3 implicitly requires everything below it.
Segregation of duties. Give Finance managers an Approver capability and Engineering managers an Executor one, and the engine can surface the subjects who'd hold both — the conflict you're contractually required to prevent — as a query, not a quarterly spreadsheet review.
Agents as first-class subjects. The fastest-growing kind of subject isn't a person — it's software acting on someone's behalf: a service account, a bot, an AI agent firing tool calls in a loop. Because anything can be a subsort of subject, an agent is authorized by exactly the same rules as a human, and — this is the part that matters when an agent acts thousands of times an hour — every action it takes comes back with the same readable proof. "This agent deleted that record" stops being a log line you have to trust and becomes a derivation you can check: which grant, inherited from which role, on whose behalf, with no denial standing in the way. An access decision you can prove rather than merely log is the difference between an audit and a guess.
None of this changes the model from the small example. It's the same sorts, the same homoiconic rules, the same backward-chaining proofs — carrying hundreds of users and dozens of services instead of one file and one group. The policy didn't get more complicated to express. The data just got bigger.
We run exactly this in a second, enterprise-scale demo: the Security Access demo loads 200 users across 30 services into the knowledge base and answers every access check with a real NAF query — denials, clearance lattice, and segregation-of-duties conflicts included.
What ReasoningLayer does differently#
Access control is a crowded field, and the good tools in it are good at what they were built for. ReasoningLayer is built around a different question — not just "is this allowed?" but "who can do this, and exactly why?" — and that changes what comes back.
Permission tables and role-based access (RBAC / ACLs). The workhorse: rows mapping who-has-what, widened by roles and groups. Fast to read, familiar to everyone — but the rows are pre-computed, so they drift the moment the org chart moves, and "why is this allowed?" is a forensic join across several tables and a layer of app code. ReasoningLayer stores the two facts and derives the rest: nothing to keep in sync, and the "why" comes back with the answer.
Policy-as-code engines (OPA / Rego, AWS Cedar). A real step up — policy becomes data: versioned, tested, reviewed, instead of if statements smeared across services. The difference is the shape of the answer. These are decision points: hand them a request, get allow or deny (with decision logs, or Cedar's determining policies, available after the fact). ReasoningLayer's answer is the derivation — the proof rides along with the verdict — and the same rule runs both directions: "can Alice view this?" and "who can view this?" are one query with the subject bound or left open. Enumerating every principal who can reach a resource isn't what a decision point is built for.
Relationship-based engines (Google Zanzibar, OpenFGA, SpiceDB). The closest relative: permissions derived from relationships, with group membership inherited exactly the way Rule 2 inherits it — and battle-scaled. ReasoningLayer adds two things they don't carry. The answer is the proof tree itself, not a boolean with a separate Expand/debug call. And it's one substrate: the same engine holds the sort and clearance lattices, the NAF denials, residuation, and the constraint-and-optimization side (the engine behind our scheduling work) — so "an approver and an executor can't be the same person, and the on-call rota must still cover every shift" is one model, not a relationship graph wired to a policy engine wired to a solver.
A third answer, for when "allow or deny" is the wrong question. Every system above returns one of two verdicts. Sometimes the honest answer is neither yet: the request can't be settled because a fact is missing — is the device managed? has the contract been counter-signed? Forced to pick, a binary engine fails open (unsafe) or fails closed (blocks legitimate work). ReasoningLayer can residuate — suspend the decision, hand back exactly what it's waiting on, and resume the instant that fact arrives. That's the difference between a flat "denied" and "denied for now, pending a managed-device check": the raw material for step-up auth instead of a dead end.
Put together: the answer is a proof, the same query runs forwards and backwards, one engine carries policy and constraints from a single file to enterprise scale, and it can say "not yet" instead of guessing. None of it is bolted on — it falls out of deriving the answer in the first place.
What this changes#
Three things get concretely easier.
Audits stop being archaeology. The "how does the intern have production access?" question becomes one query with a chain attached — not a day of tracing joins and nested groups. The proof is the audit trail, generated the moment you ask rather than reconstructed after the fact.
Revocation is one fact, not a sweep. Suspend an account and every grant it had derived evaporates on the next query, because nothing was stored to go hunt down — no "did we catch every permission that suspension was supposed to cancel?"
Agent actions become defensible. When a service account or AI agent acts thousands of times an hour, "the agent did it" has to be more than a log line. Each action carries the same proof a human's would — which grant, inherited from which role, on whose behalf — so a compliance review reads a derivation instead of taking your word for it.
Try it#
You don't have to wait for us. ▶ Open the live IAM demo — sign in to the playground, then ask who can touch a file, watch the answers come back with their proofs, and add a membership to see a new grant appear without anyone writing it down. Want it at enterprise scale? The ▶ Security Access demo runs the same machinery over 200 users and 30 services.
When you're ready to see it against your policy — your roles, your groups, your denial rules, your clearance levels — 👉 talk to us. Bring the access question that's hardest to answer today; we'll show you the proof.
Access decisions shouldn't be a black box you're asked to trust. They should be a chain you can read.
One last thing, because otherwise we'd feel like frauds: the gateway that guards ReasoningLayer is itself built on ReasoningLayer. Every call into the platform — "is this caller allowed to hit that endpoint?" — is settled by the same derive-it-and-prove-it engine you just read about. We eat our own dog food. Turns out it's pretty good.