diff --git a/docs/README.md b/docs/README.md
index 064881491b..cf63d9c80f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -72,7 +72,6 @@ It offers an HTTP API, a gRPC API, and has SDKs for programming languages includ
- [Manage Group Access](./content/interacting/managing-user-access.mdx)
- [Manage Group Membership](./content/interacting/managing-group-membership.mdx)
- [Manage Relationships Between Objects](./content/interacting/managing-relationships-between-objects.mdx)
- - [Transactional Writes](./content/interacting/transactional-writes.mdx)
- [Relationship Queries](./content/interacting/relationship-queries.mdx)
- [Get Tuple Changes](./content/interacting/read-tuple-changes.mdx)
- [Search with Permissions](./content/interacting/search-with-permissions.mdx)
diff --git a/docs/content/getting-started/perform-check.mdx b/docs/content/getting-started/perform-check.mdx
index f9c10dbc9c..ac41ad84f6 100644
--- a/docs/content/getting-started/perform-check.mdx
+++ b/docs/content/getting-started/perform-check.mdx
@@ -144,11 +144,11 @@ To obtain the [access token](https://auth0.com/docs/get-started/authentication-a
### 02. Calling Check API
-To check whether user `user:anne` has relationship `can_view` with object `document:Z`
+To check whether user `user:anne` has relationship `reader` with object `document:Z`
-This section will illustrate how to update __.
+This is an introduction to adding and deleting __.
## Before you start
@@ -93,7 +93,7 @@ Assume that you want to add user `user:anne` to have relationship `reader` with
{
user: 'user:anne',
relation: 'reader',
- object: 'document:Z',
+ object: 'document:Z'
}
```
@@ -150,7 +150,7 @@ To add the relationship tuples, we can invoke the write API.
{
user: 'user:anne',
relation: 'reader',
- object: 'document:Z',
+ object: 'document:Z'
},
]}
skipSetup={true}
@@ -175,7 +175,7 @@ Assume that you want to delete user `user:anne`'s `reader` relationship with obj
{
user: 'user:anne',
relation: 'reader',
- object: 'document:Z',
+ object: 'document:Z'
}
```
@@ -184,7 +184,7 @@ Assume that you want to delete user `user:anne`'s `reader` relationship with obj
{
user: 'user:anne',
relation: 'reader',
- object: 'document:Z',
+ object: 'document:Z'
},
]}
skipSetup={true}
@@ -199,11 +199,113 @@ Assume that you want to delete user `user:anne`'s `reader` relationship with obj
]}
/>
+### 04. Writing and deleting relationship tuples in the same request
+
+You can combine both writes and deletes in a single transactional API request. This is useful when you need to update multiple relationships atomically. All operations in the request will either succeed together or fail together.
+
+The Write API allows you to send up to `100` unique tuples in the request. (This limit applies to the sum of both writes and deletes in that request).
+
+For example, you might want to remove `user:anne` as a `writer` of `document:Z` while simultaneously updating `user:anne` as an `reader` of `document:Z`:
+
+
+
+This approach ensures that both operations succeed or fail together, maintaining transactional data consistency.
+
+
+:::note
+When using the Write API, you cannot include the same tuple (same user, relation, and object) in the writes or deletes arrays within a single request. The API will return an error with code `cannot_allow_duplicate_tuples_in_one_request` if detected.
+:::
+
+### 05. Ignoring duplicate or missing tuples
+
+Sometimes you might need to write a tuple that already exists, which would normally cause the whole request to fail. You can use the `on_duplicate: "ignore"` parameter to handle this gracefully.
+
+This is particularly useful for high-volume data imports, migrations, or ensuring certain permissions exist without complex error handling logic.
+
+For example, if you want to ensure `user:anne` has `reader` access to `document:Z` without worrying about whether the relationship already exists in :
+
+
+
+:::caution
+At the moment, this feature is only available on the API (v1.10.0). Supported SDKs will follow shortly after.
+:::
+
+Similarly, you can use `on_missing: "ignore"` when deleting tuples that might not exist.
+
+
+
+The behavior of `on_duplicate: "ignore"` is more nuanced for tuples with conditions.
+- **Identical Tuples**: If a tuple in the request is identical to an existing tuple (same user, relation, object, condition name, and condition context), it will be safely ignored.
+- **Conflicting Tuples**: If a tuple key (user, relation, object) matches an existing tuple, but the condition name or parameters are different, this is a conflict. The write attempt will be rejected, and the entire transaction will fail with a `409 Conflict` error.
+
## Related Sections
\ No newline at end of file
diff --git a/docs/content/interacting/overview.mdx b/docs/content/interacting/overview.mdx
index 9ea513c0ae..9b45bf0c8f 100644
--- a/docs/content/interacting/overview.mdx
+++ b/docs/content/interacting/overview.mdx
@@ -42,11 +42,6 @@ This section helps you integrate
-
-Using, you can update multiple in a single transaction.
-
-
-
-Updating multiple relationship tuples can keep your system state consistent.
-
-
-
-## Before you start
-
-Familiarize yourself with basic before completing this guide.
-
-
-
-
-In the following , there is called `tweet` that can have a `reader`. There is another type called `user` that can have a `follower` and `followed_by` relationship.
-
-
-
-
-
-
-
-In addition:
-
-### Direct access
-
-Creating an authorization model and a relationship tuple grants a user access to an object. To learn more, [read about Direct Access.](../modeling/direct-access.mdx)
-
-### Modeling public access
-
-The following example uses public access. To learn more, [read about Public Access.](../modeling/direct-access.mdx)
-
-### concepts
-
-- A : a class of objects that have similar characteristics
-- A : an entity in the system that can be related to an object
-- A : is a string defined in the type definition of an authorization model that defines the possibility of a relationship between an object of the same type as the type definition and a user in the system
-- A : a string defined in the type definition of an authorization model that defines the possibility of a relationship between an object of the same type as the type definition and a user in the system
-- A : a group stored in that consists of a user, a relation, and an object
-
-
-
-## Step by step
-
-### 01. Add and remove relationship tuples in the same transaction
-
-A call to the Write API can add or delete tuples in your store. For example, the following tuple makes `tweet:1` public by making everyone a `viewer`:
-
-
-
-Deleting the previous tuple converts this `tweet` to private:
-
-
-
-By removing the tuple, we made the tweet visible to no-one, which may not be what we want.
-
-
-Limitations on duplicate tuples in a single request
-
-
-When using the Write API, you cannot include the same tuple (same user, relation, and object) in both the writes and deletes arrays within a single request. The API will return an error with code `cannot_allow_duplicate_tuples_in_one_request` if duplicate tuples are detected.
-
-For example, this request would fail:
-
-```bash
-curl -X POST 'http://localhost:8080/stores/{store_id}/write' \
- -H 'content-type: application/json' \
- --data '{
- "writes": {
- "tuple_keys": [{
- "user": "user:anne",
- "relation": "member",
- "object": "group:2"
- }]
- },
- "deletes": {
- "tuple_keys": [{
- "user": "user:anne",
- "relation": "member",
- "object": "group:2"
- }]
- }
- }'
-```
-
-
-
-The Write API allows you to send up to 100 unique tuples in the request. (This limit applies to the sum of both writes and deletes in that request). This means we can submit one API call that converts the `tweet` from public to visible to only the `user`'s followers.
-
-
-
-### 02. Add multiple related relationship tuples in the same transaction
-
-Sending multiple tuples per request can also help maintain consistency. For example, if `anne` follows `becky`, you can save the following two tuples or neither of them:
-
-
-
-:::info
-In this case, the type `user` exists because users can be related to each other, so users now are a type in the system.
-:::
-
-The service attempts to perform all the changes sent in a single Write API call in one transaction. If it cannot complete all the changes, it rejects all of them.
-
-## Related Sections
-
-
diff --git a/docs/content/modeling/migrating/migrating-relations.mdx b/docs/content/modeling/migrating/migrating-relations.mdx
index bd49ca521e..44001fbb07 100644
--- a/docs/content/modeling/migrating/migrating-relations.mdx
+++ b/docs/content/modeling/migrating/migrating-relations.mdx
@@ -313,12 +313,6 @@ Now, the `write` API will only accept the new relation name.
`${JSON.stringify(tuple)}`).join(',')
- : '';
- const deleteTuples = opts.deleteRelationshipTuples
- ? opts.deleteRelationshipTuples.map((tuple) => `${JSON.stringify(tuple)}`).join(',')
- : '';
- const writes = `"writes": { "tuple_keys" : [${writeTuples}] }`;
- const deletes = `"deletes": { "tuple_keys" : [${deleteTuples}] }`;
- const separator = `${opts.deleteRelationshipTuples && opts.relationshipTuples ? ',' : ''}`;
+ // Build the JSON object for pretty printing
+ interface RequestBody {
+ writes?: {
+ tuple_keys: Array>;
+ on_duplicate?: string;
+ };
+ deletes?: {
+ tuple_keys: Array>;
+ on_missing?: string;
+ };
+ authorization_model_id?: string;
+ }
+
+ const requestBody: RequestBody = {};
+
+ if (opts.relationshipTuples?.length) {
+ requestBody.writes = {
+ tuple_keys: opts.relationshipTuples.map((tuple) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { _description, ...cleanTuple } = tuple;
+ return cleanTuple;
+ }),
+ };
+ if (opts.writeOptions?.on_duplicate) {
+ requestBody.writes.on_duplicate = opts.writeOptions.on_duplicate;
+ }
+ }
+
+ if (opts.deleteRelationshipTuples?.length) {
+ requestBody.deletes = {
+ tuple_keys: opts.deleteRelationshipTuples.map((tuple) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { _description, ...cleanTuple } = tuple;
+ return cleanTuple;
+ }),
+ };
+ if (opts.deleteOptions?.on_missing) {
+ requestBody.deletes.on_missing = opts.deleteOptions.on_missing;
+ }
+ }
+
+ // Add authorization_model_id at the end
+ requestBody.authorization_model_id = modelId;
+
+ const prettyJson = JSON.stringify(requestBody, null, 2);
+
return `curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/write \\
-H "Authorization: Bearer $FGA_API_TOKEN" \\ # Not needed if service does not require authorization
-H "content-type: application/json" \\
- -d '{${opts.relationshipTuples ? writes : ''}${separator}${
- opts.deleteRelationshipTuples ? deletes : ''
- }, "authorization_model_id": "${modelId}"}'`;
+ -d '${prettyJson}'`;
}
case SupportedLanguage.JS_SDK: {
diff --git a/static/llms.txt b/static/llms.txt
index 0524e0081e..9349a38405 100644
--- a/static/llms.txt
+++ b/static/llms.txt
@@ -100,7 +100,6 @@ OpenFGA implements authorization through:
- [Manage Group Access](https://openfga.dev/docs/interacting/managing-group-access)
- [Manage Group Membership](https://openfga.dev/docs/interacting/managing-group-membership)
- [Manage Relationships Between Objects](https://openfga.dev/docs/interacting/managing-relationships-between-objects)
- - [Transactional Writes](https://openfga.dev/docs/interacting/transactional-writes)
- [Contextual Tuples](https://openfga.dev/docs/interacting/contextual-tuples)
- [Query Consistency](https://openfga.dev/docs/interacting/consistency)
- [Relationship Queries](https://openfga.dev/docs/interacting/relationship-queries)