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 tuplesobject#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.

Authorization model schema 1.1 · the policy
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.

Relationship tuples object#relation@subject · the data
# 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.

ABAC — a condition on a relation evaluated 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.

The Check() API hot path
Request
POST /authz/check
{
  "tuple_key": {
    "user":     "user:bob",
    "relation": "viewer",
    "object":   "document:readme"
  },
  "consistency": "MINIMIZE_LATENCY"
}
Response
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.

Simulate model v7 · draft
Authorization model (draft)
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
Simulate a check
user  user:bob
relation  viewer
object  document:readme
allowed

path: 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
Ruby

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."

Relationship tuples

the ground truth

FGA enforces

Check on the hot path

ISPM analyzes

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.