How To Update Relationship Tuples
This is an introduction to adding and deleting relationship tuples.
Before you start
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have installed the SDK.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have installed the SDK.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have installed the SDK.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have installed the SDK.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have installed the SDK.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have installed the CLI.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
- You have obtained the necessary environment variables: `FGA_API_URL`, `FGA_STORE_ID`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_CLIENT_ID` and `FGA_CLIENT_SECRET`.
- You have configured the authorization model.
- You have loaded
FGA_API_URL,FGA_STORE_ID,FGA_API_TOKEN_ISSUER,FGA_API_AUDIENCE,FGA_CLIENT_IDandFGA_CLIENT_SECRETas environment variables.
Step by step
Assume that you want to add user user:anne to have relationship reader with object document:Z
{
user: 'user:anne',
relation: 'reader',
object: 'document:Z'
}
01. Configure the Auth0 FGA API client
Before calling the write API, you will need to configure the API client.
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';
// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'auth.fga.dev'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});
import (
"os"
openfga "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
"github.com/openfga/go-sdk/credentials"
)
// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'auth.fga.dev'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
func main() {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"),
StoreId: os.Getenv("FGA_STORE_ID"),
AuthorizationModelId: os.Getenv("FGA_MODEL_ID"),
Credentials: &credentials.Credentials{ // Credentials are not needed if connecting to the Playground API
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: os.Getenv("FGA_CLIENT_ID"),
ClientCredentialsClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
ClientCredentialsApiAudience: os.Getenv("FGA_API_AUDIENCE"),
ClientCredentialsApiTokenIssuer: os.Getenv("FGA_API_TOKEN_ISSUER"),
},
},
})
if err != nil {
// .. Handle error
}
}
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Client.Model;
using OpenFga.Sdk.Model;
using Environment = System.Environment;
namespace Example;
// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'auth.fga.dev'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
class MyProgram {
static async Task Main() {
var configuration = new ClientConfiguration() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"),
StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"),
AuthorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"),
Credentials = new Credentials() { // Credentials are not needed if connecting to the Playground API
Method = CredentialsMethod.ClientCredentials,
Config = new CredentialsConfig() {
ApiTokenIssuer = Environment.GetEnvironmentVariable("FGA_API_TOKEN_ISSUER"),
ApiAudience = Environment.GetEnvironmentVariable("FGA_API_AUDIENCE"),
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
ClientSecret = Environment.GetEnvironmentVariable("FGA_CLIENT_SECRET"),
}
}
};
var fgaClient = new OpenFgaClient(configuration);
}
}
import os
import openfga_sdk
from openfga_sdk.client import OpenFgaClient, ClientConfiguration
from openfga_sdk.credentials import Credentials, CredentialConfiguration
# FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
# FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
# FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
# FGA_API_TOKEN_ISSUER = 'auth.fga.dev'
# FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
# FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
# FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
async def main():
credentials = Credentials(
method='client_credentials',
configuration=CredentialConfiguration(
api_issuer= os.environ.get('FGA_API_TOKEN_ISSUER'),
api_audience= os.environ.get('FGA_API_AUDIENCE'),
client_id= os.environ.get('FGA_CLIENT_ID'),
client_secret= os.environ.get('FGA_CLIENT_SECRET'),
)
)
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. https://api.fga.example
store_id = os.environ.get('FGA_STORE_ID'), # optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
authorization_model_id = os.environ.get('FGA_MODEL_ID'), # Optional, can be overridden per request
)
# Enter a context with an instance of the OpenFgaClient
async with OpenFgaClient(configuration) as fga_client:
api_response = await fga_client.read_authorization_models()
await fga_client.close()
asyncio.run(main())
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
import dev.openfga.sdk.api.configuration.ClientCredentials;
import dev.openfga.sdk.api.configuration.Credentials;
// FGA_API_URL = 'https://api.us1.fga.dev' for Dev Preview and Early Access / 'https://api.playground.fga.dev' for the FGA Playground
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'auth.fga.dev' for Dev Preview and Early Access / not needed for the FGA Playground
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' for Dev Preview and Early Access / not needed for the FGA Playground
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page / not needed for the FGA Playground
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page / not needed for the FGA Playground
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request
.credentials(new Credentials(
new ClientCredentials()
.apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER"))
.apiAudience(System.getenv("FGA_API_AUDIENCE"))
.clientId(System.getenv("FGA_CLIENT_ID"))
.clientSecret(System.getenv("FGA_CLIENT_SECRET"))
));
var fgaClient = new OpenFgaClient(config);
}
}
Set the required environment variables
# For all environments
export FGA_STORE_ID = 'YOUR_STORE_ID' # Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
export FGA_MODEL_ID = 'YOUR_MODEL_ID' # optional, can be overridden per request, helps reduce latency
export FGA_API_URL = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
export FGA_API_TOKEN_ISSUER = 'auth.fga.dev'
export FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
export FGA_CLIENT_ID = 'YOUR_CLIENT_ID' # Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
export FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' # Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
To obtain the access token:
# Not needed when calling the Playground API
curl -X POST \
https://auth.fga.dev/oauth/token \
-H 'content-type: application/json' \
-d '{"client_id":"'$FGA_CLIENT_ID'","client_secret":"'$FGA_CLIENT_SECRET'","audience":"'$FGA_API_AUDIENCE'","grant_type":"client_credentials"}'
# The response will be returned in the form
# {
# "access_token": "eyJ...Ggg",
# "expires_in": 86400,
# "scope": "read:tuples write:tuples check:tuples ... write:authorization-models",
# "token_type": "Bearer"
# }
# Store this `access_token` value in environment variable `FGA_BEARER_TOKEN`
FGA_SERVER_URL = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
02. Calling write API to add new relationship tuples
To add the relationship tuples, we can invoke the write API.
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
};
await fgaClient.write({
writes: [
{"user":"user:anne","relation":"reader","object":"document:Z"}
],
}, options);
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
User: "user:anne",
Relation: "reader",
Object: "document:Z",
},
},
}
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",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
new() {
User = "user:anne",
Relation = "reader",
Object = "document:Z"
}
},
};
var response = await fgaClient.Write(body, options);
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:anne",
relation="reader",
object="document:Z",
),
],
)
response = await fga_client.write(body, options)
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("reader")
._object("document:Z")
));
var response = fgaClient.write(body, options).get();
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:anne reader document:Z
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": "reader",
"object": "document:Z"
}
]
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
03. Calling write API to delete relationship tuples
To delete relationship tuples, we can invoke the write API.
Assume that you want to delete user user:anne's reader relationship with object document:Z
{
user: 'user:anne',
relation: 'reader',
object: 'document:Z'
}
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
};
await fgaClient.write({
deletes: [
{ user: 'user:anne', relation: 'reader', object: 'document:Z'}
],
}, options);
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Deletes: []ClientTupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "reader",
Object: "document:Z",
},
},
}
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",
};
var body = new ClientWriteRequest() {
Deletes = new List<ClientTupleKeyWithoutCondition>() {
new() { User = "user:anne", Relation = "reader", Object = "document:Z" }
},
};
var response = await fgaClient.Write(body, options);
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
deletes=[
ClientTuple(
user="user:anne",
relation="reader",
object="document:Z",
),
],
)
response = await fga_client.write(body, options)
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.deletes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("reader")
._object("document:Z")
));
var response = fgaClient.write(body, options).get();
fga tuple delete --store-id=${FGA_STORE_ID} user:anne reader document:Z
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": "reader",
"object": "document:Z"
}
]
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
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:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
};
await fgaClient.write({
writes: [
{"user":"user:anne","relation":"reader","object":"document:Z"}
],
deletes: [
{ user: 'user:anne', relation: 'writer', object: 'document:Z'}
],
}, options);
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
User: "user:anne",
Relation: "reader",
Object: "document:Z",
},
},
Deletes: []ClientTupleKeyWithoutCondition{
{
User: "user:anne",
Relation: "writer",
Object: "document:Z",
},
},
}
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",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
new() {
User = "user:anne",
Relation = "reader",
Object = "document:Z"
}
},
Deletes = new List<ClientTupleKeyWithoutCondition>() {
new() { User = "user:anne", Relation = "writer", Object = "document:Z" }
},
};
var response = await fgaClient.Write(body, options);
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:anne",
relation="reader",
object="document:Z",
),
], deletes=[
ClientTuple(
user="user:anne",
relation="writer",
object="document:Z",
),
],
)
response = await fga_client.write(body, options)
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("reader")
._object("document:Z")
))
.deletes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("writer")
._object("document:Z")
));
var response = fgaClient.write(body, options).get();
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:anne reader document:Z
fga tuple delete --store-id=${FGA_STORE_ID} user:anne writer document:Z
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": "reader",
"object": "document:Z"
}
]
},
"deletes": {
"tuple_keys": [
{
"user": "user:anne",
"relation": "writer",
"object": "document:Z"
}
]
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
This approach ensures that both operations succeed or fail together, maintaining transactional data consistency.
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 Auth0 FGA:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
conflict: {
onDuplicateWrites: OnDuplicateWrites.Ignore,
}
};
await fgaClient.write({
writes: [
{"user":"user:anne","relation":"reader","object":"document:Z"}
],
}, 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: "reader",
Object: "document:Z",
},
},
}
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 = "reader",
Object = "document:Z"
}
},
};
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="reader",
object="document:Z",
),
],
)
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("reader")
._object("document:Z")
));
var response = fgaClient.write(body, options).get();
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:anne reader document:Z --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": "reader",
"object": "document:Z"
}
],
"on_duplicate": "ignore"
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
Similarly, you can use on_missing: "ignore" when deleting tuples that might not exist.
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
conflict: {
onMissingDeletes: OnMissingDeletes.Ignore
}
};
await fgaClient.write({
deletes: [
{ user: 'user:anne', relation: 'writer', object: 'document:Z'}
],
}, 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: "writer",
Object: "document:Z",
},
},
}
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 = "writer", Object = "document:Z" }
},
};
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="writer",
object="document:Z",
),
],
)
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("writer")
._object("document:Z")
));
var response = fgaClient.write(body, options).get();
fga tuple delete --store-id=${FGA_STORE_ID} user:anne writer document:Z --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": "writer",
"object": "document:Z"
}
],
"on_missing": "ignore"
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
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 Conflicterror.