Ignore Duplicate Tuples On Write
We've added two new optional parameters to the Write API endpoint to improve the experience of writing data to FGA. You can now gracefully "ignore" duplicate writes and missing deletes.
The Problem
When you're writing tuples to OpenFGA, it's almost inevitable that you'll try to write a relationship tuple that already exists (e.g., user:anne is already a viewer of document:123) or try to delete one that's already gone. In the past, OpenFGA would reject the entire Write request containing that single duplicate operation.
This forced developers to build complex error-handling and retry logic on the client-side, just to filter out the single problematic tuple and resend the rest of the batch. This adds latency and operational overhead.
The Solution
The Write API now accepts two new optional parameters to gracefully handle these use cases:
-
on_duplicate: "ignore": When included in thewritessection, this tells OpenFGA to simply skip any tuples that already exist instead of failing the request. -
on_missing: "ignore": When included in thedeletessection, this tells OpenFGA to skip any tuples that don't exist.
Now, you can send large batches of writes and deletes without worrying about these common conditions breaking your import.
See it in Action
For writes:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
conflict: {
onDuplicateWrites: OnDuplicateWrites.Ignore,
}
};
await fgaClient.write({
writes: [
{"user":"user:anne","relation":"viewer","object":"document:roadmap"}
],
}, options);
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
Conflict: ClientWriteConflictOptions{
OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE,
},
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
User: "user:anne",
Relation: "viewer",
Object: "document:roadmap",
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
var options = new ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
Conflict = new ConflictOptions {
OnDuplicateWrites = OnDuplicateWrites.Ignore,
}
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
new() {
User = "user:anne",
Relation = "viewer",
Object = "document:roadmap"
}
},
};
var response = await fgaClient.Write(body, options);
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA",
"conflict": ConflictOptions(
on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE,
)
}
body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:anne",
relation="viewer",
object="document:roadmap",
),
],
)
response = await fga_client.write(body, options)
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA")
.onDuplicate(WriteRequestWrites.OnDuplicateEnum.IGNORE);
var body = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("viewer")
._object("document:roadmap")
));
var response = fgaClient.write(body, options).get();
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:anne viewer document:roadmap --on-duplicate ignore
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 '{
"writes": {
"tuple_keys": [
{
"user": "user:anne",
"relation": "viewer",
"object": "document:roadmap"
}
],
"on_duplicate": "ignore"
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
And deletes:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
conflict: {
onMissingDeletes: OnMissingDeletes.Ignore
}
};
await fgaClient.write({
deletes: [
{ user: 'user:anne', relation: 'owner', object: 'document:roadmap'}
],
}, options);
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
Conflict: ClientWriteConflictOptions{
OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE,
},
}
body := ClientWriteRequest{
Deletes: []ClientTupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "owner",
Object: "document:roadmap",
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
var options = new ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
Conflict = new ConflictOptions {
OnMissingDeletes = OnMissingDeletes.Ignore
}
};
var body = new ClientWriteRequest() {
Deletes = new List<ClientTupleKeyWithoutCondition>() {
new() { User = "user:anne", Relation = "owner", Object = "document:roadmap" }
},
};
var response = await fgaClient.Write(body, options);
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA",
"conflict": ConflictOptions(
on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE
)
}
body = ClientWriteRequest(
deletes=[
ClientTuple(
user="user:anne",
relation="owner",
object="document:roadmap",
),
],
)
response = await fga_client.write(body, options)
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA")
.onMissing(WriteRequestDeletes.OnMissingEnum.IGNORE);
var body = new ClientWriteRequest()
.deletes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("owner")
._object("document:roadmap")
));
var response = fgaClient.write(body, options).get();
fga tuple delete --store-id=${FGA_STORE_ID} user:anne owner document:roadmap --on-missing ignore
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 '{
"deletes": {
"tuple_keys": [
{
"user": "user:anne",
"relation": "owner",
"object": "document:roadmap"
}
],
"on_missing": "ignore"
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
Get Started
This is supported in the latest versions of the OpenFGA API, SDKs and CLI. Try it out and let us know what you think!
Special thanks to @phamhieu for his contribution to the JavaScript SDK! 🙏
Learn more about Writing Tuples in OpenFGA.
We want your feedback!
Please reach out through our community channels with any questions or feedback.
