Implement Data Connect mutations

Firebase Data Connect lets you create connectors for your PostgreSQLinstances managed with Google Cloud SQL. These connectors are combinations of aqueries and mutations for using your data from your schema.

Note: Follow the complete series on building Data Connectschemas and connectors, which covers:

TheGet started guide introduced a moviereview app schema for PostgreSQL.

That guide also introduced both deployable and ad hoc administrative operations,including mutations.

  • Deployable mutations are those you implement to call from client apps in aconnector, with API endpoints you define.Data Connect integratesauthentication and authorization into these mutations, and generates client SDKsbased on your API.
  • Ad hoc administrative mutations are run from privileged environments topopulate and manage tables. You can create and execute them in theFirebase console, from privileged environments using theFirebase Admin SDK,and in local development environments using ourData Connect VS Code extension.

This guide takes a deeper look atdeployable mutations.

Features ofData Connect mutations

Data Connect lets you to perform basic mutations in the all the waysyou'd expect given a PostgreSQL database:

  • Perform CRUD operations
  • Manage multi-step operations with transactions

But withData Connect's extensions to GraphQL, you can implementadvanced mutations for faster, more efficient apps:

  • Usekey scalars returned by many operations to simplify repeatedoperations on records
  • Useserver values to populate data with operations provided by the server
  • Perform queries in the course of a multi-step mutation operations to look updata, saving lines of code and round trips to the server.
Note: Before using this guide, review the movie review schema.

Movie review app schema

# MoviestypeMovie@table{id:UUID!@default(expr:"uuidV4()")title:String!releaseYear:Intgenre:Stringrating:Intdescription:Stringtags:[String]}# Movie Metadata# Movie - MovieMetadata is a one-to-one relationshiptypeMovieMetadata@table{movie:Movie!@refdirector:String}//...existingcode...extendtypeMovie{movieMetadata:MovieMetadatamovieMetadatas_on_movie:MovieMetadata}# Actors# Suppose an actor can participate in multiple movies and movies can have multiple actors# Movie - Actors (or vice versa) is a many to many relationshiptypeActor@table{id:UUID!@default(expr:"uuidV4()")name:String!}# Join table for many-to-many relationship for movies and actors# The 'key' param signifies the primary keys of this table# In this case, the keys are [movieId, actorId], the foreign key fields of the reference fields [movie, actor]typeMovieActor@table{movie:Movie!actor:Actor!role:String!# "main" or "supporting"# optional other fields}typeUser@table{# `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert.id:String!@default(expr:"auth.uid")username:String!}# Reviews is a join table tween User and Movie.# It has a composite primary keys `userUid` and `movieId`.# A user can leave reviews for many movies. A movie can have reviews from many users.# User<-> Review is a one-to-many relationship# Movie<-> Review is a one-to-many relationship# Movie<-> User is a many-to-many relationshiptypeReview@table{user:User!movie:Movie!rating:IntreviewText:StringreviewDate:Date!@default(expr:"request.time")

Use generated fields to implement mutations

YourData Connect operations will extend a set of fieldsautomatically generated byData Connect based on the types and typerelationships in your schema. These fields are generated by local toolingwhenever you edit your schema.

Tip: To discover the generated fields while building operations, use the queryeditor in theFirebase console, or our Visual Studio Code extension.You can use generated fields to implement mutations, from creating,updating, and deleting individual records in single tables, to more complexmulti-table updates.

Assume your schema contains aMovie type and an associatedActor type.Data Connect generatesmovie_insert,movie_update,movie_delete fields, and more.

Note: These mutation fields return and let you pass akey scalar type, here aMovie_Key, to identify records. These are created based on yourschema.

Mutation with the
movie_insert field

Themovie_insert field represents a mutation to create a single record in theMovie table.

Use this field to create a single movie.

mutationCreateMovie($data:Movie_Data!){movie_insert(data:$data){key}}

Mutation with the
movie_update field

Themovie_update field represents a mutation to update a single record in theMovie table.

Use this field to update a single movie by its key.

mutationUpdateMovie($myKey:Movie_Key!,$data:Movie_Data!){movie_update(key:$myKey,data:$data){key}}

Mutation with the
movie_delete field

Themovie_delete field represents a mutation to delete a single record in theMovie table.

Use this field to delete a single movie by its key.

mutationDeleteMovie($myKey:Movie_Key!){movie_delete(key:$myKey){key}}
Tip: The schema definition language also lets you explicitly control how namesare generated for fields usingsingular andplural arguments for the@table directive.

Essential elements of a mutation

Data Connect mutations are GraphQL mutations withData Connectextensions. Just as with a regular GraphQL mutation, you can define an operationname and a list of GraphQL variables.

Data Connect extends GraphQL queries with customizeddirectives like@auth and@transaction.

Tip: You can use AI assistance to help you create and test GraphQL mutations intheFirebase console and in our local tooling. Learn more atUse AI assistance for scheams, queries and mutations.

So the following mutation has:

  • Amutation type definition
  • ASignUp operation (mutation) name
  • A single variable$username operation argument
  • A single directive,@auth
  • A single fielduser_insert.
mutationSignUp($username:String!)@auth(level:USER){user_insert(data:{id_expr:"auth.uid"username:$username})}

Every mutation argument requires a type declaration, a built-in likeString,or a custom, schema-defined type likeMovie.

Note: The example shows how to sign up an account as yourself. Anyonethat logs in can create a user account for themselves.

Write basic mutations

You can start writing mutations to create, update and delete individual recordsfrom your database.

Tip: You can use Gemini inFirebase to help you create and run GraphQL mutations in theFirebase console. Learn more atUse AI assistance for queries and mutations.

Create

Let's do basic creates.

# Create a movie based on user inputmutationCreateMovie($title:String!,$releaseYear:Int!,$genre:String!,$rating:Int!){movie_insert(data:{title:$titlereleaseYear:$releaseYeargenre:$genrerating:$rating})}# Create a movie with default valuesmutationCreateMovie2{movie_insert(data:{title:"Sherlock Holmes"releaseYear:2009genre:"Mystery"rating:5})}

Or an upsert.

# Movie upsert using combination of variables and literalsmutationUpsertMovie($title:String!){movie_upsert(data:{title:$titlereleaseYear:2009genre:"Mystery"rating:5genre:"Mystery/Thriller"})}

Perform updates

Here are updates. Producers and directors certainly hope that those averageratings are on trend.

Themovie_update field contains an expectedid argument to identify a recordand adata field you can use to set values in this update.

mutationUpdateMovie($id:UUID!,$genre:String!,$rating:Int!,$description:String!){movie_update(id:$id,data:{genre:$genrerating:$ratingdescription:$description})}

To perform multiple updates, use themovie_updateMany field.

# Multiple updates (increase all ratings of a genre)mutationIncreaseRatingForGenre($genre:String!,$rating:Int!){movie_updateMany(where:{genre:{eq:$genre}},data:{rating:$rating})}

Use increment, decrement, append, and prepend operations with_update

While in_update and_updateMany mutations you can explicitly set values indata:, it often makes more sense to apply an operator like increment to updatevalues.

To modify the earlier update example, assume you want to increment the ratingof a particular movie. You can userating_update syntax with theincoperator.

mutationUpdateMovie($id:UUID!,$ratingIncrement:Int!){movie_update(id:$id,data:{rating_update:{inc:$ratingIncrement}})}

Data Connect supports the following operators for field updates:

  • inc to incrementInt,Int64,Float,Date andTimestamp data types
  • dec to decrementInt,Int64,Float,Date andTimestamp data types
Note: For these operations, if an existing numeric isnull, then it is treatedas0. If an existingDate orTimestamp isnull, then it is treated asthe current date or time.

For lists, you can also update with individual values or lists of values using:

  • add to append item(s) if they are not already present to list types, except Vector lists
  • remove to remove all items, if present, from list types, except Vector lists
  • append to append item(s) to list types, except Vector lists
  • prepend to prepend item(s) to list types, except Vector lists
Note: For these operations, if the existing list field isnull, it is treatedas an empty list.Tip: For details of the interface, see the_Update and_ListUpdate reference documentation entries.

Perform deletes

You can of course delete movie data. Film preservationists will certainlywant the physical films to be maintained for as long as possible.

# Delete by keymutationDeleteMovie($id:UUID!){movie_delete(id:$id)}

Here you can use_deleteMany.

# Multiple deletesmutationDeleteUnpopularMovies($minRating:Int!){movie_deleteMany(where:{rating:{le:$minRating}})}

Write mutations on relations

Observe how to use the implicit_upsert mutation on a relation.

# Create or update a one to one relationmutationMovieMetadataUpsert($movieId:UUID!,$director:String!){movieMetadata_upsert(data:{movie:{id:$movieId},director:$director})}
Tip: The complete set of GraphQL language extensions forData Connectisdocumented in the language reference guide.

Design schemas for efficient mutations

Data Connect provides two important features that allow you towrite more efficient mutations and save round-trip operations.

Key scalars are concise object identifiers thatData Connectautomatically assembles from key fields in your schemas. Key scalars are aboutefficiency, allowing you to find in a single call information about the identityand structure of your data. They are especially useful when you want to performsequential actions on new records and need a unique identifier to pass toupcoming operations, and also when you want to access relational keys toperform additional more complex operations.

Usingserver values, you can effectively let the server dynamically populatefields in your tables using stored or readily-computable values according toparticular server-side CEL expressions in theexpr argument. For example, youcan define a field with a timestamp applied when the field is accessed usingthe time stored in an operation request,updatedAt: Timestamp!@default(expr: "request.time").

Write advanced mutations: letData Connect supply values usingfield_expr syntax

As discussed inkey scalars and server values,you can design your schema so that the server populates values for commonfields likeids and dates in response to client requests.

In addition, you can make use of data, such as user IDs, sent inData Connectrequest objects from client apps.

When you implement mutations, usefield_expr syntax to triggerserver-generated updates or access data from requests. For example, to passthe authorizationuid stored in a request to an_upsert operation, pass"auth.uid" in theuserId_expr field.

# Add a movie to the user's favorites listmutationAddFavoritedMovie($movieId:UUID!)@auth(level:USER){favorite_movie_upsert(data:{userId_expr:"auth.uid",movieId:$movieId})}# Remove a movie from the user's favorites listmutationDeleteFavoritedMovie($movieId:UUID!)@auth(level:USER){favorite_movie_delete(key:{userId_expr:"auth.uid",movieId:$movieId})}

Or, in a familiar to-do list app, when creating a new to-do list, you couldpassid_expr to instruct the server to auto-generate a UUID for the list.

mutationCreateTodoListWithFirstItem($listName:String!)@transaction{# Step 1todoList_insert(data:{id_expr:"uuidV4()",# <-- auto-generated. Or a column-level @default on `type TodoList` will also workname:$listName,})}

For more information, see the_Expr scalars in thescalars reference.

Write advanced mutations: multi-step operations

There are many situations in which you might want to include multiple writefields (like inserts) in one mutation. You might also want to read your databaseduring execution of a mutation to lookup and verify existing data beforeperforming, for example, inserts or updates. These options save round tripoperations and hence costs.

Data Connect lets you perform multi-step logic in your mutations bysupporting:

  • Multiple write fields

  • Multiple read fields in your mutations (using thequery field keyword).

  • The@transaction directive, which providestransaction support familiar from relational databases.

  • The@check directive, which letsyou evaluate the contents of reads using CEL expressions, and basedon the results of such evaluation:

    • Proceed with creates, updates and deletes defined by a mutation
    • Proceed to return the results of a query field
    • Use returned messages to perform appropriate logic in your client code
  • The@redact directive, which lets you omitquery field results from wire protocol results.

  • The CELresponse binding, which stores the accumulated results of allmutations and queries performed in a complex, multi-step operation. You canaccess theresponse binding:

    • In@check directives, through theexpr: argument
    • With server values, usingfield_expr syntax

The@transaction directive

Support for multi-step mutations includes error handling using transactions.

The@transaction directive enforces that a mutation - with either a singlewrite field (for example,_insert or_update) or with multiplewrite fields - always run in a database transaction.

  • Mutations without@transaction execute each root field one after anotherin sequence. The operation surfaces any errors as partial field errors, but notthe impacts of the subsequent executions.

  • Mutations with@transaction are guaranteed to either fully succeed or fullyfail. If any of the fields within the transaction fails, the entire transactionis rolled back.

The@check and@redact directives

The@check directive verifies that specified fields are present in queryresults. A Common Expression Language (CEL) expression is used to test fieldvalues. The default behavior of the directive is to check for and rejectnodes whose value isnull or[] (empty lists).

The@redact directive redacts a part of the response from the client. Redactedfields are still evaluated for side effects (including data changes and@check) and the results are still available to later steps in CEL expressions.

Use@check,@check(message:) and@redact

A major use for@check and@redact is looking up related data to decidewhether certain operations should be authorized, using the lookup in logic buthiding it from clients. Your query can return useful messages for correcthandling in client code.

For illustration, the following query field checks whether a requestor has anappropriate "admin" role to view users who can edit a movie.

queryGetMovieEditors($movieId:UUID!)@auth(level:USER){moviePermission(key:{movieId:$movieId,userId_expr:"auth.uid"})@redact{role@check(expr:"this == 'admin'",message:"You must be an admin to view all editors of a movie.")}moviePermissions(where:{movieId:{eq:$movieId},role:{eq:"editor"}}){user{idusername}}}

To learn more about@check and@redact directives in authorization checks,refer to thediscussion of authorization data lookup.

Use@check to validate keys

Some mutation fields, such as_update, may no-op if a record with a specifiedkey does not exist. Similarly, lookups may return null or an empty list. Theseare not considered errors and therefore won't trigger rollbacks.

To guard against this result, test whether keys can be found using the@checkdirective.

# Delete by key, error if not foundmutationMustDeleteMovie($id:UUID!)@transaction{movie_delete(id:$id)@check(expr:"this != null",message:"Movie not found, therefore nothing is deleted")}
Tip: See thedirectives reference for the@check and@redact directivesand theCEL expression reference.

Use theresponse binding to chain multi-step mutations

The basic approach to creating related records, for example a newMovie andan associatedMovieMetadata entry, is to:

  1. Call an_insert mutation forMovie
  2. Store the returned key of the created movie
  3. Then, call a second_insert mutation to create theMovieMetadata record.

But withData Connect, you can handle this common case in a singlemulti-step operation by accessing theresults of the first_insert in thesecond_insert.

Making a successful movie review app is a lot of work. Let's track our to-dolist with a new example.

Useresponse to set fields with server values

In the following to-do list mutation:

  • Theresponse binding represents the partial response object so far, whichincludes all top-level mutation fields before the current one.
  • The results of the initialtodoList_insert operation, which returns theid (key) field, are accessed later inresponse.todoList_insert.id so we canimmediately insert a new to-do item.
mutationCreateTodoListWithFirstItem($listName:String!,$itemContent:String!)@transaction{# Sub-step 1:todoList_insert(data:{id_expr:"uuidV4()",# <-- auto-generated. Or a column-level @default on `type TodoList` will also workname:$listName,})# Sub-step 2:todo_insert(data:{listId_expr:"response.todoList_insert.id"# <-- Grab the newly generated ID from the partial response so far.content:$itemContent,})}
Note: Results of sub-steps are available in the same@transaction, so youstill get the atomicity guarantees as you'd expect from a relational database.

Useresponse to validate fields using@check

response is available in@check(expr: "...") as well, so you can use it tobuild even more complicated server-side logic. Combined withquery { … } stepsin mutations, you can achieve a lot more without any additional client-serverroundtrips.

In the following example, note: that@check already has access toresponse.querybecause a@check always runs after the step it is attached to.

Tip: You can hide results of intermediate steps from the wire and clients using@redact. Redacted fields are still available inresponse for use in latersteps.
mutationCreateTodoInNamedList($listName:String!,$itemContent:String!)@transaction{# Sub-step 1: Look up List.id by its namequery@check(expr:"response.query.todoLists.size() > 0",message:"No such TodoList with the name!")@check(expr:"response.query.todoLists.size() < 2",message:"Ambiguous listName!"){todoLists(where:{name:$listName}){id}}# Sub-step 2:todo_insert(data:{listId_expr:"response.todoLists[0].id"# <-- Now we have the parent list ID to insert tocontent:$itemContent,})}

For more information about theresponse binding, see theCEL reference.

Understand interrupted operations with@transaction andquery @check

Multi-step mutations can encounter errors:

  • Database operations may fail.
  • query@check logic may terminate operations.

Data Connect recommends that you use the@transaction directive withyour multi-step mutations. This results in a more consistent database andmutation results that are easier to handle in client code:

  • At the first error or failed@check, the operation will terminate, sothere is no need to manage execution of any subsequent fields or evaluation of CEL.
  • Rollbacks are performed in response to database errors or@check logic,yielding a consistent database state.
  • A rollback error is always returned to client code.

There may be some use cases where you choose not to use@transaction: youmight opt for eventual consistency if, for example, you need higher throughput,scalability or availability. However, you need to manage your database and yourclient code to allow for the results:

  • If one field fails due to database operations, subsequent fields will continueto execute. However, failed@checks still terminate the entire operation.
  • Rollbacks are not performed, meaning a mixed database state with somesuccessful updates and some failed updates.
  • Your operations with@check may give more inconsistent results if your@check logic uses the results of reads and/or writes in a previous step.
  • The result returned to client code will contain a more complex mix of successand failure responses to be handled.

Directives forData Connect mutations

In addition to the directives you use in defining types and tables,Data Connect provides the@auth,@check,@redact and@transaction directives for augmenting the behavior of operations.

DirectiveApplicable toDescription
@authQueries and mutationsDefines the authorization policy for a query or mutation. See theauthorization and attestation guide.
@checkquery fields in multi-step operationsVerifies that specified fields are present in query results. A Common Expression Language (CEL) expression is used to test field values. SeeMulti-step operations.
@redactQueriesRedacts a part of the response from the client. SeeMulti-step operations.
@transactionMutationsEnforces that a mutation always run in a database transaction. SeeMulti-step operations.
Tip: Complete reference documentation for these directives is available in thedirectives reference guide.

Next steps

Note: This guide is part of a complete series on building Data Connect schemas and connectors, which covers:

You may be interested in:

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2026-02-10 UTC.