JazzRPC

JazzRPC is the most straightforward and complete way to securely communicate with Server Workers. It works well with any framework or runtime that supports standard Request and Response objects, can be scaled horizontally, and puts clients and workers in direct communication.

Setting up JazzRPC

Defining request schemas

Use experimental_defineRequest to define your API schema:

import { experimental_defineRequest, z } from "jazz-tools";
import { Event, Ticket } from "@/lib/schema";

const workerId = process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT!;

export const bookEventTicket = experimental_defineRequest({
  url: "/api/book-event-ticket",
  // The id of the worker Account or Group
  workerId,
  // The schema definition of the data we send to the server
  request: {
    schema: {
      event: Event,
    },
    // The data that will be considered as "loaded" in the server input
    resolve: {
      event: { reservations: true },
    },
  },
  // The schema definition of the data we expect to receive from the server
  response: {
    schema: { ticket: Ticket },
    // The data that will be considered as "loaded" in the client response
    // It defines the content that the server directly sends to the client, without involving the sync server
    resolve: { ticket: true },
  },
});

Setting up the Server Worker

We need to start a Server Worker instance that will be able to sync data with the sync server, and handle the requests.

import { startWorker } from "jazz-tools/worker";

export const jazzServer = await startWorker({
  syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
  accountID: process.env.JAZZ_WORKER_ACCOUNT,
  accountSecret: process.env.JAZZ_WORKER_SECRET,
});

Handling JazzRPC requests on the server

Creating API routes

Create API routes to handle the defined RPC requests. Here's an example using Next.js API routes:

import { jazzServer } from "@/jazzServer";
import { Ticket } from "@/lib/schema";
import { bookEventTicket } from "@/bookEventTicket";
import { Group, JazzRequestError } from "jazz-tools";

export async function POST(request: Request) {
  return bookEventTicket.handle(
    request,
    jazzServer.worker,
    async ({ event }, madeBy) => {
      const ticketGroup = Group.create(jazzServer.worker);
      const ticket = Ticket.create({
        account: madeBy,
        event,
      });

      // Give access to the ticket to the client
      ticketGroup.addMember(madeBy, "reader");

      event.reservations.$jazz.push(ticket);

      return {
        ticket,
      };
    },
  );
}

Making requests from the client

Using the defined API

Make requests from the client using the defined API:

import { bookEventTicket } from "@/bookEventTicket";
import { Event } from "@/lib/schema";
import { co, isJazzRequestError } from "jazz-tools";

export async function sendEventBookingRequest(event: co.loaded<typeof Event>) {
  const { ticket } = await bookEventTicket.send({ event });

  return ticket;
}

export async function sendEventBookingRequest(event: co.loaded<typeof Event>) {
  try {
    const { ticket } = await bookEventTicket.send({ event });

    return ticket;
  } catch (error) {
    // This works as a type guard, so you can easily get the error message and details
    if (isJazzRequestError(error)) {
      alert(error.message);
      return;
    }
  }
}

Error handling

Server-side error handling

Use JazzRequestError to return proper HTTP error responses:

export async function POST(request: Request) {
  return bookEventTicket.handle(
    request,
    jazzServer.worker,
    async ({ event }, madeBy) => {
      // Check if the event is full
      if (event.reservations.length >= event.capacity) {
        // The JazzRequestError is propagated to the client, use it for any validation errors
        throw new JazzRequestError("Event is full", 400);
      }

      const ticketGroup = Group.create(jazzServer.worker);
      const ticket = Ticket.create({
        account: madeBy,
        event,
      });

      // Give access to the ticket to the client
      ticketGroup.addMember(madeBy, "reader");

      event.reservations.$jazz.push(ticket);

      return {
        ticket,
      };
    },
  );
}
Note

To ensure that the limit is correctly enforced, the handler should be deployed in a single worker instance (e.g. a single Cloudflare DurableObject).

Details on how to deploy a single instance Worker are available in the Deployment patterns section.

Client-side error handling

Handle errors on the client side:

export async function sendEventBookingRequest(event: co.loaded<typeof Event>) {
  try {
    const { ticket } = await bookEventTicket.send({ event });

    return ticket;
  } catch (error) {
    // This works as a type guard, so you can easily get the error message and details
    if (isJazzRequestError(error)) {
      alert(error.message);
      return;
    }
  }
}
Note

The experimental_defineRequest API is still experimental and may change in future versions. For production applications, consider the stability implications.

Security safeguards provided by JazzRPC

JazzRPC includes several built-in security measures to protect against common attacks:

Cryptographic Authentication

  • Digital Signatures: Each RPC is cryptographically signed using the sender's private key
  • Signature Verification: The server verifies the signature using the sender's public key to ensure message authenticity and to identify the sender account
  • Tamper Protection: Any modification to the request payload will invalidate the signature

Replay Attack Prevention

  • Unique Message IDs: Each RPC has a unique identifier (co_z${string})
  • Duplicate Detection: incoming messages ids are tracked to prevent replay attacks
  • Message Expiration: RPCs expire after 60 seconds to provide additional protection

These safeguards ensure that JazzRPC requests are secure, authenticated, and protected against common attack vectors while maintaining the simplicity of standard HTTP communication.

See also

Was this page helpful?