Fine-Grained Access
From roles to relationships.
Authorize every object.
Relationship- and attribute-based access control beyond RBAC. Model who can do what to which object the way Google Zanzibar and OpenFGA do, ask Check() on the request hot path, and reuse the same authorization graph to find who's over-privileged.
Available on Pro and above. See pricing
The model
Relationships in, decisions out.
Authorization is two things: a model that declares the relations on each object type, and a set of relationship tuples — object#relation@subject — that are the data. A relation is computed: directly assigned, unioned with another relation, or rewritten across an edge like viewer from parent. You write facts about your data; Check() resolves the graph for you.
model
schema 1.1
type user
type team
relations
define member: [user]
type folder
relations
define viewer: [user, team#member]
type document
relations
define parent: [folder]
define owner: [user]
define editor: [user] or owner
define viewer: [user, team#member]
or editor
or viewer from parent A userset rewrite. editor unions owner; viewer unions editor and pulls viewer down from the parent folder.
# Alice is an editor of the readme doc document:readme#editor@user:alice # Anyone on the eng team can view it (a userset subject) document:readme#viewer@team:eng#member # The readme lives in the handbook folder document:readme#parent@folder:handbook # Folder viewers inherit view on its documents folder:handbook#viewer@user:bob
A subject can be one user or a whole group. team:eng#member is a userset — resolved at check time.
condition non_expired(now: timestamp, expires_at: timestamp) {
now < expires_at
}
# grant editor, but only until the tuple expires
document:readme#editor@user:alice
with non_expired { "expires_at": "2026-09-01T00:00:00Z" } Contextual parameters and tuple attributes — time-bound, device-bound, or value-bound access — are checked inside the same engine. No second policy system to keep in sync.
Beyond RBAC
Roles can't see the object.
RBAC is fine until access depends on which object — this document, that folder, this customer's records. The role table has no edge to the thing being accessed, so the answer drifts into per-service code and nobody can audit it. Fine-Grained Access adds relationship-based (ReBAC) and attribute-based (ABAC) authorization on top of the roles you already have — three questions, one graph.
RBAC asks "what role?"
Roles are coarse — global by design. "Editor" everywhere, or a sprawl of custom roles no one can reason about. A role can't say Alice may edit this one document because her team owns the folder it lives in. The object never enters the decision.
ReBAC asks "what relationship?"
Permission is computed from relationships between objects — owner, editor, viewer, parent folder, team membership. The model resolves the graph at check time. This is the model behind Google Zanzibar; OpenFGA implements it in the open.
ABAC asks "under what conditions?"
Layer attribute predicates on a relation: only before an expiry, only from a managed device, only above a dollar threshold. Conditions and contextual tuples are evaluated inside the same Check — one engine, not a separate rules system to keep in sync.
Default-deny, per object
Every check starts from no. Access exists only where a relationship tuple — direct or rewritten — grants it. A leaked or over-broad role can't silently open a door that was never relationship-connected to the object.
Aligned with the model behind Google Zanzibar. RBAC, ReBAC, and ABAC on one authorization graph — not three systems to keep in sync.
The API surface
Check is the start, not the end.
One graph answers four questions. Check guards a request; ListObjects populates a screen; ListUsers drives access reviews; Expand explains exactly why. A consistency parameter trades latency against read-after-write correctness, so a freshly written grant is visible the moment it matters.
Check
Check(user, relation, object) → bool The hot-path question on every request: can this subject do this thing to this object? Resolves directly-assigned tuples and userset rewrites to a single allow/deny — default-deny, in single-digit milliseconds.
ListObjects
ListObjects(user, relation, type) → [object] Invert the question: which documents can Alice view? Returns the set of objects of a type the subject has a relation to — so a list screen renders without firing N separate Checks.
ListUsers
ListUsers(object, relation) → [user] Who can edit this object? Expands usersets to the concrete subjects. The basis for share dialogs, access reviews, and effective-permission reports.
Expand
Expand(object, relation) → tree The full userset tree behind a grant — every union, intersection, and rewrite that contributes. The answer to "why does this subject have access?" and the debugger for an authorization model.
POST /authz/check
{
"tuple_key": {
"user": "user:bob",
"relation": "viewer",
"object": "document:readme"
},
"consistency": "MINIMIZE_LATENCY"
} 200 OK
{
"allowed": true,
"resolution": "document:readme#viewer <- viewer from parent <- folder:handbook#viewer <- user:bob"
} Bob never had a direct grant. The engine rewrites viewer through the parent folder, returns the path, and stays default-deny everywhere else.
Model it · simulate it · ship it
Design the model. Test before you ship.
The authorization model is versioned policy — every change produces a new model ID. The console gives you a model editor, a tuple browser, and a simulate playground: run a Check against a draft model and see the answer plus the resolution path that produced it, before a single line of application code changes. Promote the model when the simulation is green.
model
schema 1.1
type user
type team
relations
define member: [user]
type folder
relations
define viewer: [user, team#member]
type document
relations
define parent: [folder]
define owner: [user]
define editor: [user] or owner
define viewer: [user, team#member]
or editor
or viewer from parentpath: viewer from parent → folder:handbook#viewer
Change a rule, re-run, see the path move — a regression test for access, not a production incident.
The model is policy-as-data with a version on it. Simulate, diff, and roll it out like code.
Seven SDKs
One Check, every language.
Object-level enforcement is one call from a request handler — not a policy DSL bolted onto each service. check() ships in all seven AuthFI SDKs with the same default-deny semantics everywhere, so the authorization decision lives in the graph and never drifts between services.
// Node.js — guard a route on a per-object relationship
const ok = await authfi.fga.check({
user: `user:${session.userId}`,
relation: 'editor',
object: `document:${params.id}`,
});
if (!ok) return res.status(403).end();Node.js, Go, Python, Java, C#, PHP, and Ruby. The same check() and the same model behind every one.
Enforce & audit
The graph that enforces is the graph that audits.
Most products bolt governance on after the fact and infer effective permissions from role tables. AuthFI doesn't have to — the relationship tuples that enforce a request are the exact same tuples ISPM reads to analyze it. One decision log, one ground truth, one answer to "what can this identity actually reach?"
Over-privilege analysis
The same tuples that authorize a request are the ground truth for who is over-privileged. ISPM reads the authorization graph directly instead of inferring access from role assignments.
Effective permissions, computed
ListObjects and Expand give a precise answer to "what can this identity actually reach?" — across inherited relations and userset rewrites, not just the direct grants a role table would show.
Blast-radius from real grants
Walk the graph from a compromised identity to every object it can touch. Blast radius stops being an estimate and becomes a traversal over live tuples.
Real least-privilege
Unused-access detection compares granted relations against the relations actually checked. FGA + ISPM closes the gap between "has a role" and "needs the access."
the ground truth
Check on the hot path
over-privilege & blast radius
One graph, enforced and audited. Least-privilege stops being a slide and becomes a query.
Get started
Ready to get started?
Free for 5,000 monthly active users. No credit card required.