You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hi all, opening this as a discussion (not an issue) because it's an architectural question rather than a concrete feature ask. I've done some homework on the codebase first; flagging upfront where my understanding might be wrong.
The observation
OpenFGADatastore (specifically the embedded RelationshipTupleReader in pkg/storage/storage.go) exposes only flat tuple operations — Read, ReadPage, ReadUserTuple, ReadUsersetTuples, ReadStartingWithUser. That's a deliberate, well-justified design: it keeps the contract minimal and the four backends (memory / Postgres / MySQL / SQLite) easy to keep in sync.
The flip side is that graph expansion is ultimately expressed as repeated tuple-reader calls from the check / list-objects paths (internal/check/ and internal/graph/) to storage, and the traversal is reconstructed in Go from those flat tuple sets. The authorization model graph already knows the traversal shape, but storage only sees independent tuple-reader calls.
For most workloads this is fine. For models with deep userset chains, large group fan-out, or intersection / exclusion over usersets (#1931), the round-trip count and the inability of the storage engine to "see" the traversal shape might become a real cost. I haven't measured it.
It's also visible from the outside: community projects keep popping up that try to back FGA-style authorization with graph-shaped storage (e.g. relation_fga compiling models to Cypher against Memgraph). That's not evidence the approach works — but it's evidence the question recurs.
The narrow ask first
Before proposing any API, the more useful question is: would the team be interested in a benchmark that isolates whether deep-userset / TTU / intersection / exclusion cases are bottlenecked by repeated tuple-reader round-trips versus resolver-side graph expansion in Go?
If that benchmark shows the round-trips dominate on realistic pathological models, there's a design conversation worth having. If it shows resolver-side expansion dominates (or the existing wrappers like iterator_cache already absorb the cost), then the answer is "no, this does not justify changing the storage abstraction" — and that's a good answer to have on record.
The bigger question, contingent on the above
If the benchmark validates a real round-trip cost, would the project consider letting backends opt into a separate, optional capability interface — not additions to OpenFGADatastore itself — that some backends could implement with a fallback path through today's flat operations?
Sketch — illustrative shape only, not a proposed API:
// Optional, separate from OpenFGADatastore. Backends signal support// via a capability check; the check engine feature-detects and falls// back to the flat interface when absent.//// (Types like UsersetRef, ModelGraphRef, ExpandOptions etc. are// placeholders — the point is the *shape* of the primitive, not the// signature.)typeGraphTraversalBackendinterface {
// Walk a userset subtree against the authorization model graph,// returning leaf subjects in a single round-trip.ExpandUserset(ctx context.Context, storestring, refUsersetRef,
modelGraphModelGraphRef, optsExpandOptions,
) (TupleIterator, error)
// Given a user and an object type/relation, return reachable objects// through the model graph in one call.ReachableObjects(ctx context.Context, storestring, userUserRef,
objectType, relationstring, modelGraphModelGraphRef,
) (ObjectIterator, error)
}
Keeping the capability separate from OpenFGADatastore is load-bearing here — if it's added to the main interface, every backend, every mock, and every wrapper has to implement it, which kills the "optional" framing.
Acknowledged complication: wrapper composition
The most obvious complication is that this would have to compose with the tuple-reader wrappers in pkg/storage/storagewrappers/ — bounded_datastore, cached_datastore / cached_reader, iterator_cache, combinedtuplereader (contextual tuples), model_caching, request, plus consistency handling, throttling, and condition / CEL filtering. If feature-detection runs against the raw datastore, it bypasses those wrappers. If it runs against the wrapper, the capability disappears unless every wrapper forwards it.
I don't have a clean answer here — it's the part of the design that would need the most thought. Possible options: forwarding shims on every wrapper; a wrapper-aware capability interface that includes the relevant context (contextual tuples, consistency level, condition filters) as parameters; or restricting the capability to paths that don't need wrapper-injected behaviour. None are obviously right.
What this isn't asking for
Not asking for Neo4j / Memgraph / Dgraph / JanusGraph / Neptune to be added as a supported backend. Adding a backend is heavy: full conformance test suite, ULID-ordered changelog for the Watch API, transactional writes, mock generation, ongoing maintenance. That cost shouldn't be paid speculatively.
Not asking to replace the flat interface — it's the right primitive for the existing backends and serves the common case well.
Not asking to relax any consistency or Consistency / ContinuationToken semantics.
Where I might be wrong
I may be over-estimating the per-level round-trip cost. If the existing wrappers (especially iterator_cache and cached_reader) already absorb most of it on realistic workloads, the answer is just "no, it's not a real problem."
The relation_fga project I linked is not a controlled comparison — it's two different systems with different consistency models. I'm using it as evidence that the question recurs, not as evidence that graph storage is faster.
I haven't profiled an actual OpenFGA deployment under a pathological model; I'm reasoning from the code, not from numbers. That's exactly why the narrow ask is for a benchmark.
Some of this may have shifted with the newer internal/check/ path; corrections welcome.
What would be useful feedback
Has this design space been discussed before (internal RFC, closed thread, design doc)? My searches found nothing in this repo or the discussions board.
Independent of any API change — would a benchmark isolating round-trip cost vs. resolver-side cost on a known-pathological model be useful to the project, or has this already been measured and dismissed?
If "yes, that benchmark would be useful" — is there a preferred shape for it? (e.g. existing benchmark harness in the repo, a specific model + tuple distribution the team would trust as representative)
If the benchmark validated a real cost, is there appetite in principle for a separate optional capability interface, or is the team committed to keeping all backends behind a single uniform OpenFGADatastore?
If "no" to any of the above — totally fair, and an authoritative answer here will save several people from re-asking this same question every six months.
Happy to do the legwork on (2)/(3) if there's interest.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Hi all, opening this as a discussion (not an issue) because it's an architectural question rather than a concrete feature ask. I've done some homework on the codebase first; flagging upfront where my understanding might be wrong.
The observation
OpenFGADatastore(specifically the embeddedRelationshipTupleReaderinpkg/storage/storage.go) exposes only flat tuple operations —Read,ReadPage,ReadUserTuple,ReadUsersetTuples,ReadStartingWithUser. That's a deliberate, well-justified design: it keeps the contract minimal and the four backends (memory / Postgres / MySQL / SQLite) easy to keep in sync.The flip side is that graph expansion is ultimately expressed as repeated tuple-reader calls from the check / list-objects paths (
internal/check/andinternal/graph/) to storage, and the traversal is reconstructed in Go from those flat tuple sets. The authorization model graph already knows the traversal shape, but storage only sees independent tuple-reader calls.For most workloads this is fine. For models with deep userset chains, large group fan-out, or
intersection/exclusionover usersets (#1931), the round-trip count and the inability of the storage engine to "see" the traversal shape might become a real cost. I haven't measured it.It's also visible from the outside: community projects keep popping up that try to back FGA-style authorization with graph-shaped storage (e.g.
relation_fgacompiling models to Cypher against Memgraph). That's not evidence the approach works — but it's evidence the question recurs.The narrow ask first
Before proposing any API, the more useful question is: would the team be interested in a benchmark that isolates whether deep-userset / TTU / intersection / exclusion cases are bottlenecked by repeated tuple-reader round-trips versus resolver-side graph expansion in Go?
If that benchmark shows the round-trips dominate on realistic pathological models, there's a design conversation worth having. If it shows resolver-side expansion dominates (or the existing wrappers like
iterator_cachealready absorb the cost), then the answer is "no, this does not justify changing the storage abstraction" — and that's a good answer to have on record.The bigger question, contingent on the above
If the benchmark validates a real round-trip cost, would the project consider letting backends opt into a separate, optional capability interface — not additions to
OpenFGADatastoreitself — that some backends could implement with a fallback path through today's flat operations?Sketch — illustrative shape only, not a proposed API:
Keeping the capability separate from
OpenFGADatastoreis load-bearing here — if it's added to the main interface, every backend, every mock, and every wrapper has to implement it, which kills the "optional" framing.Acknowledged complication: wrapper composition
The most obvious complication is that this would have to compose with the tuple-reader wrappers in
pkg/storage/storagewrappers/—bounded_datastore,cached_datastore/cached_reader,iterator_cache,combinedtuplereader(contextual tuples),model_caching,request, plus consistency handling, throttling, and condition / CEL filtering. If feature-detection runs against the raw datastore, it bypasses those wrappers. If it runs against the wrapper, the capability disappears unless every wrapper forwards it.I don't have a clean answer here — it's the part of the design that would need the most thought. Possible options: forwarding shims on every wrapper; a wrapper-aware capability interface that includes the relevant context (contextual tuples, consistency level, condition filters) as parameters; or restricting the capability to paths that don't need wrapper-injected behaviour. None are obviously right.
What this isn't asking for
Consistency/ContinuationTokensemantics.Where I might be wrong
iterator_cacheandcached_reader) already absorb most of it on realistic workloads, the answer is just "no, it's not a real problem."relation_fgaproject I linked is not a controlled comparison — it's two different systems with different consistency models. I'm using it as evidence that the question recurs, not as evidence that graph storage is faster.internal/check/path; corrections welcome.What would be useful feedback
OpenFGADatastore?Happy to do the legwork on (2)/(3) if there's interest.
All reactions