Skip to content

RayfoldOne request per screen, and every screen stays right

An API protocol for queries, commands and live data. Commands tell clients what they changed, so nothing refetches. The schema says who may do what and what a request may cost, and the server enforces it. For TypeScript, React, Angular, Kotlin, Java and Spring Boot.

Start in three steps

  1. Install the server and the clientnpm install @rayfold/server @rayfold/client
  2. Describe your API in a .rayfold schema: types, operations and their rules
  3. Serve it with createRayfoldServer and call it with RayfoldClient

The quickstart, step by step On the JVM: Kotlin, Java, Spring Boot

The same bookshop, built three ways and measured. Fewer is better.
TaskRESTGraphQLRayfold
Round trips to order and pay221
Refetches to keep a list right after a write110
Duplicate orders when a retry has no key110
Author lookups for 20 books, written plainly9201
Pieces you write for live stock341
Extra parts to serve an AI agent220
Rayfold leads on 15 of 15 aspects measured end to end, each cell asserted by a test. Every row, and where the others are fine

What it looks like ​

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

rayfold
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

json
{
  "rayfold": "0.1",
  "ops": [
    { "id": 1, "op": "buy",
      "args": { "bookId": "b1" },
      "key": "7f3c9a2e4b1d4c6f" },
    { "id": 2, "op": "book",
      "args": { "id": { "$ref": "1.id" } },
      "shape": "{ title stock author { name } }",
      "live": true }
  ]
}

Frames back

json
{ "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.

Before and after ​

The server says what each write changed. The code that keeps screens correct after a write is no longer needed. Here is one feature written the usual way and then in Rayfold: buy a book, show it, and leave every other open screen correct.

Before. Nothing here is wrong or unusual. This is typical client code today.

ts
// 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.

ts
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 them

Two round trips become one. Step 2 travels in the same request and takes its argument from step 1's result. Step 3 is gone, because the command's answer carries the patch. Add live: true to the query and another customer's purchase arrives as a patch too, with no second system to run.

What you give up: every operation has to be declared in a schema first, and your team learns a new protocol. Should you use Rayfold? says when that is worth it and when it is not. The comparison shows where REST and GraphQL reach the same result with more work.

Pick your stack ​

Every guide builds the same small bookshop. Every example project runs its tests on each change to the repository, so the code you copy works.

Released under the Apache-2.0 license.