From a4782f84ad2264229c01472c54c128f59bfa59ff Mon Sep 17 00:00:00 2001 From: Tyler Nix Date: Wed, 13 May 2026 22:46:36 -0500 Subject: [PATCH 1/6] new blog on upcoming changes for weighted graph models --- blog/weighted-graph-upcoming-changes.md | 264 ++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 blog/weighted-graph-upcoming-changes.md diff --git a/blog/weighted-graph-upcoming-changes.md b/blog/weighted-graph-upcoming-changes.md new file mode 100644 index 0000000000..81eeee57e3 --- /dev/null +++ b/blog/weighted-graph-upcoming-changes.md @@ -0,0 +1,264 @@ +--- +title: "OpenFGA's Move to Weighted Graph Resolution: What's Changing" +description: OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models. +slug: weighted-graph-upcoming-changes +date: 2026-05-14 +authors: tylernix +tags: [announcement, check, migration] +image: https://openfga.dev/img/og-rich-embed.png +hide_table_of_contents: false +--- + +# OpenFGA's Move to Weighted Graph Resolution: What's Changing + +OpenFGA is continuing to roll out a **weighted graph-based resolution algorithm** across its core query endpoints. ListObjects already runs on this update algorithm, and Check is next. As a precaution, both endpoints currently fall back to the legacy algorithm for models that are incompatible with the weighted graph — but **that fallback option will be removed soon**. A date for the final changeover has not been set at this time. + +This post explains which modeling and check patterns are incompatible with the weighted graph algorithm, and how to migrate before the fallback is removed. + + +## Why a Weighted Graph? + +The legacy Check algorithm resolves authorization queries by recursively traversing relation definitions at *request time*. While functional, this approach has limitations: + +- **Unpredictable resource usage**: A shallow recursive graph can be more resource-intensive than a deep linear one, but the old algorithm used a fixed depth limit of 26 as its only complexity guard. +- **Non-deterministic error handling**: Errors in one branch of a union could halt evaluation of other valid branches. + +The new approach shifts some resolution work earlier to *build time*: when a model is saved, the weighted graph is built and sub-graph weights are calculated for every relation. These weights reflect the relative complexity of traversing each part of the graph. At request time, the algorithm uses those pre-calculated weights to traverse the graph more efficiently — prioritizing lower-cost paths and managing load without arbitrary depth limits. + +A key consequence of this shift is that **if a model cannot be resolved, it will not be built**. Problems that previously surfaced at query time are now caught at model validation time, before any requests are made. + +The benefits, however, are: + +1. **Better performance** — resolution paths are informed by pre-calculated weights rather than discovered through live traversal. +2. **Dynamic load management** — datastore throttling, graph flattening, and context cancellation replace the fixed depth limit of 26. +3. **Consistent short-circuit evaluation** — failing fast on intersection/exclusion errors while being resilient to errors in union branches. +4. **Determinism by construction** — patterns that cannot be reliably resolved are rejected when the model is saved, not when a user makes a request. + +## What's Changing + +### Model Build Errors + +The following model patterns are incompatible with the weighted graph algorithm. Today, requests using these models fallback to the legacy algorithm. When the fallback is eventually removed, these models will fail to build and return a model build error. + +#### 1. Missing Relation + +When a relation uses `relation from parent` and the `parent` relation allows multiple types, **every type must define the referenced relation**. Previously, a TTU was resolved at query time. If a relation didn’t exist, it would skip it and return no results (`allowed: false`). The new weighted graph builds the complete resolution graph at model build time. When the relation for a TTU doesn’t exist, there’s no node to connect, and the graph fails to build. + +**Broken pattern:** + +```dsl +type organization + relations + define member: [user] + +type folder + relations + define viewer: [user] + # ❌ Missing: member is not defined here + +type document + relations + define parent: [organization, folder] + define viewer: member from parent # but folder has no member, only viewer! +``` + +**Why this fails:** Access checks through `folder` as `parent` silently return `false` — users never get access even if they should. + +**Fix 1:** Add the missing relation to the type. If the type has an equivalent role, you can alias it: + +```dsl +type folder + relations + define viewer: [user] + define member: viewer # alias of viewer +``` + +> **Migration impact:** No tuple changes required. The model change is additive — existing `viewer` tuples on `folder` objects continue to work, and the new `member` alias picks them up automatically. No check call changes needed either, since `viewer from parent` already resolves through the new `member` relation. + +**Fix 2:** Alternatively, you can separate out the `parent` relation in `type document` into explicit per-type relations. This is more verbose but makes the intent explicit in the model. + +```dsl +type document + relations + define organization: [organization] + define folder: [folder] + define viewer: member from organization or viewer from folder +``` + +> **Migration impact:** This is a breaking tuple change. All existing `(folder:x, parent, document:y)` and `(organization:x, parent, document:y)` tuples must be deleted and rewritten as `(folder:x, folder, document:y)` and `(organization:x, organization, document:y)` respectively. Check calls that reference `parent` also need to be updated if your application uses that relation directly. + + +#### 2. Tuple Cycles in Intersection or Exclusion + +Recursive relations (like nested group membership) that use `and` or `but not` create cycles that cannot be resolved deterministically. + +**Broken pattern (AND):** + +```dsl +type group + relations + define approved: [user, group#member] + define member: [user, group#member] and approved # ❌ Cycle with AND +``` + +**Broken pattern (BUT NOT):** + +```dsl +type group + relations + define blocked: [user, group#member] + define member: [user, group#member] but not blocked # ❌ Cycle with BUT NOT +``` + +**Why this fails:** To check if a user is a `member`, the system must resolve `group#member` (recursion) while simultaneously checking the `and`/`but not` condition — which itself depends on `member` resolution. This creates a circular dependency. + +**Fix:** Split into a "base" relation (allows recursion) and an "allowed" relation (applies the access gate): + +```dsl +type group + relations + define blocked: [user, group#member] + define member: [user, group#member] # Pure union recursion (allowed) + define allowed_member: member but not blocked # Exclusion at leaf level only +``` + +**Migration impact:** Your existing `member` and `blocked` tuples stay exactly as they are — no tuple writes or deletes needed. The only change is in how your application calls Check: replace `check(object, "member", user)` with `check(object, "allowed_member", user)`. The new `allowed_member` relation reads from the same underlying data, just with the exclusion gate applied at the right level. + +### Check Request Errors + +#### 3. Userset or Wildcard Requests with Exclusion + +Sometimes you want to ask "Does this whole group have access?" (i.e. Userset `document:contract#owner`) or "Does everyone have access?" (i.e. Wildcard `user:*`) by passing the group reference or wildcard as the user in a check call. However, when the relation being checked uses `but not`, OpenFGA can't reliably answer that question. To be sure, it would need to check every individual in that group or every possible user to confirm none of them are excluded. Rather than guess, it will return an **error**. + +**Userset example:** + +```dsl +type document + relations + define owner: [user] + define member: [user] + define viewer: [document#owner] but not member +``` + +``` +check("document:report", "viewer", "document:contract#owner") +# ❌ Error — can't confirm every owner passes the exclusion +``` + +**Wildcard example:** + +```dsl +type document + relations + define public: [user:*] + define blocked: [user] + define viewer: public but not blocked +``` + +``` +check("document:readme", "viewer", "user:*") +# ❌ Error — can't confirm no one in user:* is blocked +``` + +**Fix:** Check specific users individually instead: + +``` +check("document:report", "viewer", "user:alice") # ✓ — userset exclusion applied correctly per user +check("document:readme", "viewer", "user:alice") # ✓ — wildcard exclusion applied correctly per user +``` + +--- + +### Check Resolution Changes + +These changes affect the same Check request pattern as #3: passing a group reference (e.g., userset `document:d1#viewer`) as the user. But in these scenarios, the request does not error — it completes, but may return a different answer than before. Since most applications check access for a specific user (`user:alice`), these scenarios are rare and likely won't encounter these, but are still worth mentioning. + +#### 4. Userset Must Exist + +When you write a tuple granting a group access to something, the relation name used in the check must use the same as the tuple written. If your model defines two names as equivalent (i.e. aliases), that equivalence is not used when looking up group access — only the exact name stored in the tuple. + +**What was broken:** The legacy algorithm would follow aliases in the model at resolution time by inferring they were equivalent and bridging the gap. The issue appears when subtly renaming a relation or restructuring an alias, silently changing what access checks returned. + +**What is changing:** The weighted graph resolves this by making stored tuples the authoritative source of truth. Alias traversal is no longer performed during a check — what's stored is what counts. This makes access decisions deterministic and independent of how relations happen to be defined at check time. + +```dsl +type document + relations + define reader: [user] + define allowed: reader # allowed is an alias for reader + define viewer: [user, document#allowed] +``` + +``` +# Stored tuple: {document:source#allowed, viewer, document:target} + +check("document:target", "viewer", "document:source#allowed") +# ✓ Returns TRUE — matches what's stored + +check("document:target", "viewer", "document:source#reader") +# ❌ Returns FALSE — the stored tuple only uses #allowed, not #reader +``` + +**Fix:** Check using the relation name that's actually stored in the tuple. If you need to check both names, store a tuple for each: + +``` +write(document:source#allowed, viewer, document:target) # Already stored +write(document:source#reader, viewer, document:target) # Store explicitly +``` + +> **Migration impact:** +> 1. First, check whether your model has any relations that accept a group reference as a value — look for type lists that include `type:object#relation` (e.g., `define viewer: [user, document#allowed]`). If none of your relations accept group references, this change does not affect you. +> 2. If they do exist in your model, audit your check calls for any that pass a group reference as the user. Verify that the relation name in the check matches the relation name used when the tuple was written. If your application relied on alias inference — checking with `#reader` when `#allowed` was stored — update those calls to use the stored relation name, or write additional tuples to explicitly cover the names you check with. + +#### 5. Self-Referential Usersets + +**What was broken:** Previously, asking "do the viewers of document A have access to document A?" `check("document:A", "viewer", "document:A#viewer")` always returned `TRUE`, just because the relation existed in the model, not because an actual tuple granted that access. + +**What is changing:** The weighted graph requires both schema and data to grant access — a relation existing in the model is not sufficient evidence that a group has access to an object. Now it returns `FALSE` unless there's an actual tuple granting that access. + +``` +check("document:d1", "viewer", "document:d1#viewer") +# ❌ Always returned TRUE before, now returns FALSE (no tuple data exists) +``` + +**Fix:** Check individual users instead: + +``` +check("document:d1", "viewer", "user:alice") # ✓ Checks actual data +``` + +> **Migration impact:** This pattern — asking whether a group defined on an object has access to that same object — is uncommon. Search your application for check calls where the object and the group reference share the same type and ID. If you find any, replace them with checks against specific users, or use ListUsers to find who actually has the relation. + +--- + +## How to Check If You're Model Is Affected + +1. **Model validation**: Run `fga model validate` against your model. If it reports errors about missing relations or tuple cycles with AND/BUT NOT, your model needs updating. + +2. **Userset/wildcard checks**: Audit your application for check requests that use: + - Usersets (`type:id#relation`) against relations with `but not` + - Wildcards (`user:*`) against relations with `but not` + - Self-referential patterns (`check(X, rel, X#rel)`) + +3. **Test your models**: Use `fga model test` with `.fga.yaml` test files to validate expected behavior. + +## Timeline + +**Now**: +- ListObjects runs on the weighted graph algorithm, with a fallback to the legacy algorithm for incompatible models. + +**Next**: +- Check transitions to the weighted graph algorithm as default, also with a fallback. +- Provide a CLI command `fga model validate` to test if your model contains any issues and needs a migration. +- Date announced for final deadline to migrate models before new versions of OpenFGA will only support weighted graph algorithm. + +**Later**: +- Weighted graph algorithm is enforced, and the fallback algorithm is removed. Incompatible models will fail to build and requests will be rejected. + +## Get Help + +We want to hear from you — if these changes affect your deployment, reach out in our community channels and we'll help you migrate. + +- [OpenFGA Community Slack](https://openfga.dev/docs/community) +- [GitHub Discussions](https://github.com/orgs/openfga/discussions) + From a5d9684c423999b77af01a1f110baa62c0a1ac5d Mon Sep 17 00:00:00 2001 From: Tyler Nix Date: Wed, 13 May 2026 22:54:21 -0500 Subject: [PATCH 2/6] cleanup --- blog/weighted-graph-upcoming-changes.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/blog/weighted-graph-upcoming-changes.md b/blog/weighted-graph-upcoming-changes.md index 81eeee57e3..8c2a8a747f 100644 --- a/blog/weighted-graph-upcoming-changes.md +++ b/blog/weighted-graph-upcoming-changes.md @@ -231,17 +231,6 @@ check("document:d1", "viewer", "user:alice") # ✓ Checks actual data --- -## How to Check If You're Model Is Affected - -1. **Model validation**: Run `fga model validate` against your model. If it reports errors about missing relations or tuple cycles with AND/BUT NOT, your model needs updating. - -2. **Userset/wildcard checks**: Audit your application for check requests that use: - - Usersets (`type:id#relation`) against relations with `but not` - - Wildcards (`user:*`) against relations with `but not` - - Self-referential patterns (`check(X, rel, X#rel)`) - -3. **Test your models**: Use `fga model test` with `.fga.yaml` test files to validate expected behavior. - ## Timeline **Now**: From b0095e761c019f3439436c1a7eb46b0e10427831 Mon Sep 17 00:00:00 2001 From: Tyler Nix Date: Wed, 13 May 2026 23:16:47 -0500 Subject: [PATCH 3/6] expanding #5 use case to highlight the 3 scenarios it could show up --- blog/weighted-graph-upcoming-changes.md | 101 +++++++++++++++++++----- 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/blog/weighted-graph-upcoming-changes.md b/blog/weighted-graph-upcoming-changes.md index 8c2a8a747f..6a39364f35 100644 --- a/blog/weighted-graph-upcoming-changes.md +++ b/blog/weighted-graph-upcoming-changes.md @@ -1,9 +1,9 @@ --- title: "OpenFGA's Move to Weighted Graph Resolution: What's Changing" -description: OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models. -slug: weighted-graph-upcoming-changes -date: 2026-05-14 -authors: tylernix +description: "OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models." +slug: check-resolution-weighted-graph +date: 2026-XX-XX +authors: tyler tags: [announcement, check, migration] image: https://openfga.dev/img/og-rich-embed.png hide_table_of_contents: false @@ -122,7 +122,7 @@ type group define allowed_member: member but not blocked # Exclusion at leaf level only ``` -**Migration impact:** Your existing `member` and `blocked` tuples stay exactly as they are — no tuple writes or deletes needed. The only change is in how your application calls Check: replace `check(object, "member", user)` with `check(object, "allowed_member", user)`. The new `allowed_member` relation reads from the same underlying data, just with the exclusion gate applied at the right level. +> **Migration impact:** Your existing `member` and `blocked` tuples stay exactly as they are — no tuple writes or deletes needed. The only change is in how your application calls Check: replace `check(user, "member", object)` with `check(user, "allowed_member", object)`. The new `allowed_member` relation reads from the same underlying data, just with the exclusion gate applied at the right level. ### Check Request Errors @@ -141,7 +141,7 @@ type document ``` ``` -check("document:report", "viewer", "document:contract#owner") +check("document:contract#owner", "viewer", "document:report") # ❌ Error — can't confirm every owner passes the exclusion ``` @@ -156,15 +156,15 @@ type document ``` ``` -check("document:readme", "viewer", "user:*") +check("user:*", "viewer", "document:readme") # ❌ Error — can't confirm no one in user:* is blocked ``` **Fix:** Check specific users individually instead: ``` -check("document:report", "viewer", "user:alice") # ✓ — userset exclusion applied correctly per user -check("document:readme", "viewer", "user:alice") # ✓ — wildcard exclusion applied correctly per user +check("user:alice", "viewer", "document:report") # ✓ — userset exclusion applied correctly per user +check("user:alice", "viewer", "document:readme") # ✓ — wildcard exclusion applied correctly per user ``` --- @@ -192,10 +192,10 @@ type document ``` # Stored tuple: {document:source#allowed, viewer, document:target} -check("document:target", "viewer", "document:source#allowed") +check("document:source#allowed", "viewer", "document:target") # ✓ Returns TRUE — matches what's stored -check("document:target", "viewer", "document:source#reader") +check("document:source#reader", "viewer", "document:target") # ❌ Returns FALSE — the stored tuple only uses #allowed, not #reader ``` @@ -212,25 +212,90 @@ write(document:source#reader, viewer, document:target) # Store explicitly #### 5. Self-Referential Usersets -**What was broken:** Previously, asking "do the viewers of document A have access to document A?" `check("document:A", "viewer", "document:A#viewer")` always returned `TRUE`, just because the relation existed in the model, not because an actual tuple granted that access. +**What was broken:** Previously, asking "do the viewers of document A have access to document A?" like `check("document:A#viewer", "viewer", "document:A")`, the legacy algorithm always evaluated `TRUE`, just because the relation existed in the model, not because an actual tuple granted that access. -**What is changing:** The weighted graph requires both schema and data to grant access — a relation existing in the model is not sufficient evidence that a group has access to an object. Now it returns `FALSE` unless there's an actual tuple granting that access. +**What is changing:** The weighted graph requires both schema and data to grant access. A relation existing in the model is not sufficient evidence that a group has access to an object. Now it returns `FALSE` unless actual tuples exist to support the access decision. The weighted graph algorithm requires both schema and data, not schema alone. + +This can be represented in three scenarios: + +**Scenario A — Direct relation:** + +```dsl +type document + relations + define viewer: [user] +``` ``` -check("document:d1", "viewer", "document:d1#viewer") -# ❌ Always returned TRUE before, now returns FALSE (no tuple data exists) +check("document:d1#viewer", "viewer", "document:d1") +# ❌ OLD: TRUE (just because the viewer relation exists on type document) +# ✓ NEW: FALSE (since no tuple grants document:d1#viewer access to document:d1) ``` -**Fix:** Check individual users instead: +**Scenario B — Computed relation:** + +```dsl +type document + relations + define editor: [user] + define writer: [user] + define viewer: editor or writer +``` ``` -check("document:d1", "viewer", "user:alice") # ✓ Checks actual data +check("document:d1#writer", "viewer", "document:d1") +# ❌ OLD: TRUE (model has viewer = editor or writer, and writer exists on type document) +# ✓ NEW: FALSE (since no tuple grants document:d1#writer as a viewer of document:d1) ``` -> **Migration impact:** This pattern — asking whether a group defined on an object has access to that same object — is uncommon. Search your application for check calls where the object and the group reference share the same type and ID. If you find any, replace them with checks against specific users, or use ListUsers to find who actually has the relation. +**Scenario C — TTU (tuple-to-userset) relation:** + +```dsl +type folder + relations + define viewer: [user] + +type document + relations + define parent: [folder] + define viewer: viewer from parent +``` + +``` +# Given tuple: (folder:f2, parent, document:d1) + +check("folder:f2#viewer", "viewer", "document:d1") +# ❌ OLD: TRUE (the parent tuple exists and the schema connects them) +# ✓ NEW: FALSE (since no explicit userset tuple stores folder:f2#viewer as viewer of document:d1) +``` + +**Fix:** Check individual users instead of self-referential group references: + +``` +check("user:alice", "viewer", "document:d1") # ✓ Checks actual data +``` + +Or use ListUsers to discover who has the relation: + +``` +listUsers("document:d1", "viewer") → [user:alice, user:bob] +``` + +> **Migration impact:** This self-referential userset pattern — asking whether a group defined on an object has access to that same object — is uncommon. Search your application for check calls where the user field is a group reference and the object and the group reference share the same type and ID. If you find any, replace them with checks against specific users, or use ListUsers to find who actually has the relation. --- +## How to Check If You're Model Is Affected + +1. **Model validation**: Run `fga model validate` against your model. If it reports errors about missing relations or tuple cycles with AND/BUT NOT, your model needs updating. + +2. **Userset/wildcard checks**: Audit your application for check requests that use: + - Usersets (`type:id#relation`) against relations with `but not` + - Wildcards (`user:*`) against relations with `but not` + - Self-referential patterns (`check(X#rel, rel, X)`) + +3. **Test your models**: Use `fga model test` with `.fga.yaml` test files to validate expected behavior. + ## Timeline **Now**: From 1724d9eead46bcdd5dde9ac6862417af1737212b Mon Sep 17 00:00:00 2001 From: Tyler Nix Date: Wed, 13 May 2026 23:27:11 -0500 Subject: [PATCH 4/6] typos and quick fixes --- blog/weighted-graph-upcoming-changes.md | 63 ++++++++++--------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/blog/weighted-graph-upcoming-changes.md b/blog/weighted-graph-upcoming-changes.md index 6a39364f35..327a34c02c 100644 --- a/blog/weighted-graph-upcoming-changes.md +++ b/blog/weighted-graph-upcoming-changes.md @@ -11,7 +11,7 @@ hide_table_of_contents: false # OpenFGA's Move to Weighted Graph Resolution: What's Changing -OpenFGA is continuing to roll out a **weighted graph-based resolution algorithm** across its core query endpoints. ListObjects already runs on this update algorithm, and Check is next. As a precaution, both endpoints currently fall back to the legacy algorithm for models that are incompatible with the weighted graph — but **that fallback option will be removed soon**. A date for the final changeover has not been set at this time. +OpenFGA is continuing to roll out a **weighted graph-based resolution algorithm** across its core query endpoints. ListObjects already runs on this updated algorithm, and Check is next. As a precaution, both endpoints currently fall back to the legacy algorithm for models that are incompatible with the weighted graph — but **that fallback option will be removed soon**. A date for the final changeover has not been set at this time. This post explains which modeling and check patterns are incompatible with the weighted graph algorithm, and how to migrate before the fallback is removed. @@ -38,7 +38,7 @@ The benefits, however, are: ### Model Build Errors -The following model patterns are incompatible with the weighted graph algorithm. Today, requests using these models fallback to the legacy algorithm. When the fallback is eventually removed, these models will fail to build and return a model build error. +The following model patterns are incompatible with the weighted graph algorithm. Today, requests using these models fall back to the legacy algorithm. When the fallback is eventually removed, these models will fail to build and return a model build error. #### 1. Missing Relation @@ -46,7 +46,7 @@ When a relation uses `relation from parent` and the `parent` relation allows mul **Broken pattern:** -```dsl +```dsl.openfga type organization relations define member: [user] @@ -66,18 +66,18 @@ type document **Fix 1:** Add the missing relation to the type. If the type has an equivalent role, you can alias it: -```dsl +```dsl.openfga type folder relations define viewer: [user] define member: viewer # alias of viewer ``` -> **Migration impact:** No tuple changes required. The model change is additive — existing `viewer` tuples on `folder` objects continue to work, and the new `member` alias picks them up automatically. No check call changes needed either, since `viewer from parent` already resolves through the new `member` relation. +> **Migration impact:** No tuple changes required. The model change is additive — existing `viewer` tuples on `folder` objects continue to work, and the new `member` alias picks them up automatically. No check call changes needed either, since `member from parent` already resolves through the new `member` relation. **Fix 2:** Alternatively, you can separate out the `parent` relation in `type document` into explicit per-type relations. This is more verbose but makes the intent explicit in the model. -```dsl +```dsl.openfga type document relations define organization: [organization] @@ -94,7 +94,7 @@ Recursive relations (like nested group membership) that use `and` or `but not` c **Broken pattern (AND):** -```dsl +```dsl.openfga type group relations define approved: [user, group#member] @@ -103,7 +103,7 @@ type group **Broken pattern (BUT NOT):** -```dsl +```dsl.openfga type group relations define blocked: [user, group#member] @@ -114,7 +114,7 @@ type group **Fix:** Split into a "base" relation (allows recursion) and an "allowed" relation (applies the access gate): -```dsl +```dsl.openfga type group relations define blocked: [user, group#member] @@ -132,7 +132,7 @@ Sometimes you want to ask "Does this whole group have access?" (i.e. Userset `do **Userset example:** -```dsl +```dsl.openfga type document relations define owner: [user] @@ -140,14 +140,14 @@ type document define viewer: [document#owner] but not member ``` -``` +```text check("document:contract#owner", "viewer", "document:report") # ❌ Error — can't confirm every owner passes the exclusion ``` **Wildcard example:** -```dsl +```dsl.openfga type document relations define public: [user:*] @@ -155,14 +155,14 @@ type document define viewer: public but not blocked ``` -``` +```text check("user:*", "viewer", "document:readme") # ❌ Error — can't confirm no one in user:* is blocked ``` **Fix:** Check specific users individually instead: -``` +```text check("user:alice", "viewer", "document:report") # ✓ — userset exclusion applied correctly per user check("user:alice", "viewer", "document:readme") # ✓ — wildcard exclusion applied correctly per user ``` @@ -171,7 +171,7 @@ check("user:alice", "viewer", "document:readme") # ✓ — wildcard exclusion a ### Check Resolution Changes -These changes affect the same Check request pattern as #3: passing a group reference (e.g., userset `document:d1#viewer`) as the user. But in these scenarios, the request does not error — it completes, but may return a different answer than before. Since most applications check access for a specific user (`user:alice`), these scenarios are rare and likely won't encounter these, but are still worth mentioning. +These changes affect the same Check request pattern as #3: passing a group reference (e.g., userset `document:d1#viewer`) as the user. But in these scenarios, the request does not error — it completes, but may return a different answer than before. Since most applications check access for a specific user (`user:alice`), these scenarios are rare and most applications likely won't encounter them, but are still worth mentioning. #### 4. Userset Must Exist @@ -181,7 +181,7 @@ When you write a tuple granting a group access to something, the relation name u **What is changing:** The weighted graph resolves this by making stored tuples the authoritative source of truth. Alias traversal is no longer performed during a check — what's stored is what counts. This makes access decisions deterministic and independent of how relations happen to be defined at check time. -```dsl +```dsl.openfga type document relations define reader: [user] @@ -189,7 +189,7 @@ type document define viewer: [user, document#allowed] ``` -``` +```text # Stored tuple: {document:source#allowed, viewer, document:target} check("document:source#allowed", "viewer", "document:target") @@ -201,7 +201,7 @@ check("document:source#reader", "viewer", "document:target") **Fix:** Check using the relation name that's actually stored in the tuple. If you need to check both names, store a tuple for each: -``` +```text write(document:source#allowed, viewer, document:target) # Already stored write(document:source#reader, viewer, document:target) # Store explicitly ``` @@ -220,13 +220,13 @@ This can be represented in three scenarios: **Scenario A — Direct relation:** -```dsl +```dsl.openfga type document relations define viewer: [user] ``` -``` +```text check("document:d1#viewer", "viewer", "document:d1") # ❌ OLD: TRUE (just because the viewer relation exists on type document) # ✓ NEW: FALSE (since no tuple grants document:d1#viewer access to document:d1) @@ -234,7 +234,7 @@ check("document:d1#viewer", "viewer", "document:d1") **Scenario B — Computed relation:** -```dsl +```dsl.openfga type document relations define editor: [user] @@ -242,7 +242,7 @@ type document define viewer: editor or writer ``` -``` +```text check("document:d1#writer", "viewer", "document:d1") # ❌ OLD: TRUE (model has viewer = editor or writer, and writer exists on type document) # ✓ NEW: FALSE (since no tuple grants document:d1#writer as a viewer of document:d1) @@ -250,7 +250,7 @@ check("document:d1#writer", "viewer", "document:d1") **Scenario C — TTU (tuple-to-userset) relation:** -```dsl +```dsl.openfga type folder relations define viewer: [user] @@ -261,7 +261,7 @@ type document define viewer: viewer from parent ``` -``` +```text # Given tuple: (folder:f2, parent, document:d1) check("folder:f2#viewer", "viewer", "document:d1") @@ -271,13 +271,13 @@ check("folder:f2#viewer", "viewer", "document:d1") **Fix:** Check individual users instead of self-referential group references: -``` +```text check("user:alice", "viewer", "document:d1") # ✓ Checks actual data ``` Or use ListUsers to discover who has the relation: -``` +```text listUsers("document:d1", "viewer") → [user:alice, user:bob] ``` @@ -285,17 +285,6 @@ listUsers("document:d1", "viewer") → [user:alice, user:bob] --- -## How to Check If You're Model Is Affected - -1. **Model validation**: Run `fga model validate` against your model. If it reports errors about missing relations or tuple cycles with AND/BUT NOT, your model needs updating. - -2. **Userset/wildcard checks**: Audit your application for check requests that use: - - Usersets (`type:id#relation`) against relations with `but not` - - Wildcards (`user:*`) against relations with `but not` - - Self-referential patterns (`check(X#rel, rel, X)`) - -3. **Test your models**: Use `fga model test` with `.fga.yaml` test files to validate expected behavior. - ## Timeline **Now**: From e14c366884c5ad49803ead91a15ca6ed1fa2707f Mon Sep 17 00:00:00 2001 From: Tyler Nix Date: Wed, 13 May 2026 23:30:12 -0500 Subject: [PATCH 5/6] more final fixes --- blog/weighted-graph-upcoming-changes.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blog/weighted-graph-upcoming-changes.md b/blog/weighted-graph-upcoming-changes.md index 327a34c02c..c575c17f72 100644 --- a/blog/weighted-graph-upcoming-changes.md +++ b/blog/weighted-graph-upcoming-changes.md @@ -1,9 +1,9 @@ --- -title: "OpenFGA's Move to Weighted Graph Resolution: What's Changing" -description: "OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models." -slug: check-resolution-weighted-graph -date: 2026-XX-XX -authors: tyler +title: OpenFGA's Move to Weighted Graph Resolution: What's Changing +description: OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models. +slug: weighted-graph-upcoming-changes +date: 2026-05-14 +authors: tylernix tags: [announcement, check, migration] image: https://openfga.dev/img/og-rich-embed.png hide_table_of_contents: false From da7c6c1615e89182182c722749b6c0f3ba17a593 Mon Sep 17 00:00:00 2001 From: Tyler Nix Date: Wed, 13 May 2026 23:34:20 -0500 Subject: [PATCH 6/6] fixing build errors --- blog/weighted-graph-upcoming-changes.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blog/weighted-graph-upcoming-changes.md b/blog/weighted-graph-upcoming-changes.md index c575c17f72..daeb61c20a 100644 --- a/blog/weighted-graph-upcoming-changes.md +++ b/blog/weighted-graph-upcoming-changes.md @@ -1,6 +1,6 @@ --- -title: OpenFGA's Move to Weighted Graph Resolution: What's Changing -description: OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models. +title: "OpenFGA's Move to Weighted Graph Resolution: What's Changing" +description: "OpenFGA is transitioning to a weighted graph-based resolution algorithm. Learn what's changing, why, and how to migrate your models." slug: weighted-graph-upcoming-changes date: 2026-05-14 authors: tylernix @@ -15,6 +15,7 @@ OpenFGA is continuing to roll out a **weighted graph-based resolution algorithm* This post explains which modeling and check patterns are incompatible with the weighted graph algorithm, and how to migrate before the fallback is removed. + ## Why a Weighted Graph?