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 materializationsThere 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", {
: "POST",
: { "content-type": "application/json" },
: JSON.stringifyinput,
};
if!response.ok {
throw new Errorawait response.text;
}
const payload = await response.json;
applyCatalogDiffpayload.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:
- mutation commands and their input types
- authorization and business invariants
- SQL or ORM writes and transaction boundaries
- reporting every physical relation changed by the transaction
- returning or publishing the resulting catalog diff
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.