Mutations and live updates

RAQ does not turn a relational query into an INSERT, UPDATE, or DELETE. Your application still owns writes, including validation, authorization, transactions, and business rules. RAQ’s job begins after the application knows which physical relations the write changed.

The seemingly magical part is automatic invalidation: the server translates physical write lineage into public catalog changes, and the client uses those changes to refresh the subscribed queries that can be affected.

The complete write path

form or action
  -> application mutation endpoint
     -> validate and write in one transaction
     -> record the physical relations touched
     -> catalog maps those relations to public surfaces
  <- result + semantic catalog diff
     -> React runtime refreshes affected materializations

There are no inferred business mutations in this path. An action such as accepting a quote may update a quote and create an order, order lines, and an invoice. Application code performs that workflow explicitly. It then reports the relations it actually touched.

Return lineage with the mutation

The Python demo’s product-create endpoint writes products, inventory, pricing, and product_metrics in one transaction. It appends those four physical events to the change log before committing, then returns the write result together with the public diff:

{
  "result": {
    "id": 10001,
    "inventory_id": 10001
  },
  "catalog": {
    "cursor": 4,
    "changes": [
      {
        "scope": "products",
        "surfaces": ["products"],
        "operations": ["insert"],
        "version": 4
      }
    ]
  }
}

The browser sees the public products surface, not the names of its four physical dependencies. This preserves the same catalog boundary used for reads.

How the catalog derives the diff

Each catalog surface is already a relational tree. RAQ walks that tree to find its physical sources. If a recorded mutation intersects those dependencies, the catalog emits a change for the public surface.

For example, the public products surface joins several physical relations:

public products surface
  <- products
  <- inventory
  <- pricing
  <- product_metrics
  <- categories
  <- brands
  ...

An insert into inventory can therefore affect products. The application does not maintain a second list of frontend cache keys, and the client does not need to know that the join exists.

The current diff is deliberately conservative. It says that a public surface may have changed; it is not a row patch and does not prove that a particular filtered query changed. That keeps the boundary correct for joins, filters, ordering, and limits.

Apply the diff in React

Pass the catalog part of a successful write response to the shared runtime:

import { applyCatalogDiff } from "@raquery/react";

async function createProduct(input) {
  const response = await fetch("/admin/products", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(input),
  });

  if (!response.ok) {
    throw new Error(await response.text());
  }

  const payload = await response.json();
  applyCatalogDiff(payload.catalog);
  return payload.result;
}

applyCatalogDiff checks active materializations by their public query sources. Unaffected sources are left alone. Affected materializations are satisfied by safe semantic reuse when possible and otherwise refetched. Equivalent queries still share one materialization, so every subscribing component observes the same update.

This is why the UI can feel automatic: components declare reads with useRaq, while mutation code reports one semantic diff. Neither side coordinates named cache entries by hand.

Catch up after missing a response

A tab can miss a write response while it is in the background or while another client performs the mutation. The monotonically increasing cursor supports a separate catch-up path:

import { reconcileCatalogChanges } from "@raquery/react";

window.addEventListener("focus", () => {
  void reconcileCatalogChanges();
});

The runtime requests GET /catalog/changes?since=<cursor> and applies the returned diff through the same path. Polling, server-sent events, or a WebSocket can transport the same catalog-change payload; transport is separate from the relational invalidation model.

What you still implement

RAQ does not replace your mutation layer. The application remains responsible for:

RAQ supplies the dependency translation and shared-query refresh behavior. The result is less cache bookkeeping, not hidden write logic.

Run the React catalog-diff lab to see the full path, then read React for the query subscription API.