One request per screen
Send several operations together, and let a later one use an earlier one's result. Placing an order and showing it is one round trip.
Batches
Describe the API once. Screens ask for exactly the data they show, stay current without fetching again, and the schema decides who can do what. For TypeScript, React, Kotlin, Java and Spring Boot.
Send several operations together, and let a later one use an earlier one's result. Placing an order and showing it is one round trip.
Batches
Every command returns patches for what it changed, and the client cache applies them. Add live true to a query and other people's changes arrive too.
Live updates
Who may read or change what, what a request may cost and how long a result may be cached are declared once and enforced on every request.
Who can do what
A loader gets every parent at once, so a page of 50 books loads its authors in one call, not 50.
Queries and shapes
Commands declare what can go wrong. Clients handle OutOfStock by name, with its data, instead of parsing a message.
Commands and errors
JSON over POST, cacheable GET, REST routes, an OpenAPI document, and an MCP endpoint so AI agents can use the same API.
Coming from REST
A schema, a request for one screen, and what comes back. The same request works against a TypeScript, Kotlin, Java or Spring Boot server.
The schema
entity Book {
id: ID
title: String
stock: Int
author: Author
}
query book(id: ID): Book?
command buy(bookId: ID, qty: Int = 1): Book
throws OutOfStock
@allow(write: viewer != null)One request, two steps
{
"rayfold": "0.1",
"ops": [
{ "id": 1, "op": "buy",
"args": { "bookId": "b1" } },
{ "id": 2, "op": "book",
"args": { "id": { "$ref": "1.id" } },
"shape": "{ title stock author { name } }",
"live": true }
]
}Frames back
{ "id": 1,
"ok": { "$type": "Book", "id": "b1",
"title": "A Wizard of Earthsea", "stock": 2 },
"patch": [{ "set": "Book:b1", "value": {
"$type": "Book", "id": "b1",
"title": "A Wizard of Earthsea", "stock": 2 } }],
"meta": { "cost": 1 }, "fin": true }
{ "id": 2,
"data": { "$type": "Book",
"title": "A Wizard of Earthsea", "stock": 2,
"author": { "$type": "Author",
"name": "Ursula K. Le Guin" } },
"meta": { "cost": 2 } }
{ "id": 2,
"patch": [{ "at": "", "value": { "stock": 7 } }] }The last frame comes later, when a member of staff restocks the book: the query was sent with live: true, so the server keeps it open and sends only what changed.
The advantage is not that the requests are smaller. It is that the server says what changed, so the code that keeps screens correct after a write stops existing. Here is the same feature — buy a book, show it, and leave every other open screen correct — written the usual way and then in Rayfold.
Before. Nothing here is wrong or unusual; this is what careful code looks like today.
// 1. the write
await client.mutate({ mutation: BUY, variables: { bookId: "b3", qty: 2 } });
// 2. the read it implies: a second round trip, because the mutation's result
// does not carry the fields this screen shows
const { data } = await client.query({
query: BOOK,
variables: { id: "b3" },
fetchPolicy: "network-only",
});
// 3. every other open view holding that book is now stale. Either refetch them,
// or write a cache update for each, and keep that list correct as screens are added
await client.refetchQueries({ include: [CATALOGUE, CART_BADGE, STOCK_BANNER] });After.
const batch = client.batch();
const bought = batch.command<Book>("buy", { bookId: "b3", qty: 2 }, { shape: "{ id }" });
const book = batch.query<Book>("book", { id: bought.ref("id") }, { shape: "{ title stock }" });
await batch.run();
const { title, stock } = await book.promise;
// step 3 is not shortened, it is gone: `buy` answered with patches for what it
// changed, and every cached view holding that book has already applied themTwo round trips become one, because step 2 travels in the same request and takes its argument from step 1's result. Step 3 disappears, because the command's answer carries the patch. Add live: true to the query and another customer's purchase arrives through the same mechanism, with no second system to run.
The cost of that: your API has to be described in a schema, and your team learns one more protocol. Should you use Rayfold? is the honest version of that trade, and the comparison says which rows are defaults rather than things the alternatives cannot do.
Every guide builds the same small bookshop, and every example project runs its tests on each change to the repository, so the code you copy is code that works.