a dead-simple 😵, secure 🔐, type-safe 🦄 RPC client and server
powered by the XMTP messaging protocol.
npm install @qrpc/quiver
or yarn add @qrpc/quiver
or pnpm add @qrpc/quiver
- Serve a function.
- Call the function.
- That's it!
// server.ts
import quiver from "@qrpc/quiver";
import { xmtp } from "./xmtp.js";
const q = quiver.q();
q.serve(() => 42);
console.log(`Server running at ${q.address}`)
// client.ts
import quiver from "@qrpc/quiver";
const q = quiver.q();
const client = q.client(process.env.SERVER_ADDRESS);
const answer = await client();
console.log(answer.data); // 42
That's all there is to it 🎉, you've just deployed a function to the internet, and called that function, in ~10 lines of code! No DNS, AWS, GCP, LOL, or WTF's involved! To learn more, keep on reading! To see more advanced examples, jump ahead to the Advanced Examples section. If you're wondering where the magic happens, jump to Under the Hood.
- Quickstart
- Table of Contents
- Features
- Basic Usage
- Middleware Guide
- API Reference
- Off-the-Shelf Middlewares
- Advanced Examples
- Under the Hood
- Roadmap
quiver
is an extremely simple way to rapidly build and deploy services to the internet. It's powered by XMTP and inspired by trpc.
- Type-Safe Client/Server
- Type-Safe Middleware
- Fluent builder APIs
- End-to-End Encryption
- Dead-Simple
quiver
lets you rapidly build secure client/server applications. The simplest server is just a function. A QuiverFunction
can take 0, 1, or 2 arguments and optionally return a value. We always refer to the first argument as props
and the second argument as context
. Here's a simple example without:
// server.ts
import quiver from "@qrpc/quiver";
const q = quiver.q();
q.serve((props: { a: number, b: number }) => {
return add(props);
});
You'll probably want to serve more than just a single function. You can do this by using a QuiverRouter
. quiver
provides a type-safe fluent-style builder API for constructing routers. Here's a simple example:
// server.ts
import { q } from "./q";
const router = q.router()
.function("a", () => "a")
.function("b", () => "b")
q.serve(router);
And your client can call these functions:
// client.ts
import { q } from "./q";
const client = q.client(process.env.SERVER_ADDRESS);
const a = await client.a(); // { data: "a" }
const b = await client.b(); // { data: "b" }
Routers can of course be nested into a tree structure. Here's an example:
// router.ts
import { q } from "./q";
const hello = q.router()
.function("a", () => "hello from a")
.function("b", () => "hello from b")
const goodbye = q.router()
.function("a", () => "goodbye from a")
.function("b", () => "goodbye from b")
export const router = q.router()
.router("hello", hello)
.router("goodbye", goodbye)
And now your client mirrors the structure of the server:
// client.ts
import { q } from "./q";
const client = q.client(process.env.SERVER_ADDRESS);
await client.hello.a(); // { data: "hello from a" }
await client.hello.b(); // { data: "hello from b" }
await client.goodbye.a(); // { data: "goodbye from a" }
await client.goodbye.b(); // { data: "goodbye from b" }
quiver
provides a simple but powerful middleware system. A QuiverMiddleware
is a function that takes 0 or 1 arguments and optionally returns an object. We always refer to the argument as context
. Here's a simple example:
// middleware.ts
import { q } from "./q";
const logger = q.middleware(ctx => {
console.log(ctx);
});
We can attach middleware to router and functions with use
:
import { q } from "./q";
import { logger, timestamp } from "./middleware";
import { fn } from "./fn";
import { router } from "./router";
const fnWithTimestamp = fn.use(logger);
const routerWithLogger = router.use(logger);
const root = routerWithLogger.function("fn", fnWithTimestamp);
q.serve(root);
When a quiver
server receives a request, it derives a default context object from the request and then passes it through the server's middleware. More details on this process can be found in the Middleware section.
quiver
's entire backend API is fully type-safe by default as long as you annotate all arguments. quiver
's client API (q.client
) is also fully type-safe whenever you provide the backend's type to the client. Here's an example of how to provide the backend's type to the client:
// router.ts
import { q } from "./q";
const router = q.router()
.function("a", (i: { name: string }) => `hello, ${i.name}`)
.function("b", () => "hello from b")
// Export the type of the router
export type Router = typeof router;
q.serve(router);
// client.ts
// Import the Router type
import type { Router } from "./router";
import { q } from "./q";
// Notice the generic here.
const client = q.client<Router>(process.env.SERVER_ADDRESS);
Now your client is type-safe! If you try to call a function that doesn't exist, you'll get a TypeScript error, if you pass the wrong arguments, you'll get a TypeScript error, and the return value's data
field will be correctly typed!
So far in all the examples, an XMTP network client is created inside our initial call to quiver.q()
. This means that your server is listening to a random address. You'll probably want to re-use the same address (at least in production). You can do this by manually initializing XMTP and passing it to quiver
. Here's how:
// server.ts
import quiver from "@qrpc/quiver";
const xmtp = quiver.x({ init: { key: process.env.XMTP_SECRET_KEY } });
const q = quiver.q({ xmtp });
q.serve(() => 42);
Now the server will be running at whatever address corresponds to your XMTP_SECRET_KEY
, and you can call it:
// client.ts
import quiver from "@qrpc/quiver";
const quiver = quiver.q();
const client = quiver.client(process.env.SERVER_ADDRESS);
const answer = await client(); // { data: 42 }
quiver
supports a simple but powerful type-safe middleware API. A QuiverMiddleware
is essentially a function that optinally takes a context object and optionally returns a context object. When a middleware takes a context object, we say it "reads" from the context. When a middleware returns a context object, we say it "writes" to the context. We think of it this way because each middleware's return value is merged into the context which it receives.
// middleware.ts
import { q } from "./q";
const logger = q.middleware(ctx => {
console.log(ctx);
});
const timestamp = q.middleware(() => {
return {
timestamp: Date.now(),
};
});
To use a middleware in your server, you attach it to a QuiverRouter
or QuiverFunction
. Here's an example with a router:
import { logger } from "./middleware";
const router = q.router()
.use(logger)
.function("a", () => "a")
.function("b", () => "b")
quiver
's middleware system is type-safe. If you try to bind incompatible routes to a router with middleware, you'll get a TypeScript error:
import { q } from "./q";
const passesAString = q.middleware(ctx => {
return {
a: "a",
};
});
const needsANumber = (i: undefined, ctx: { a: number }) => {
// ...
}
const router = q.router()
.use(passesAString)
// Boom! TypeScript error!
.function("a", needsANumber)
Middleware can be merged in a type-safe manner using mw.extend(other)
and mw.pipe(other)
. extend
can be thought of as "parallel merge" and pipe
can be thought of as "sequential merge". Some examples:
import { q } from "./q";
const a = q.middleware(() => {
return { a: Math.random(), };
});
const b = q.middleware(() => {
return { b: Math.random(), };
});
const sum = q.middleware((ctx: { a: number, b: number }) => {
return {
sum: ctx.a + ctx.b,
};
});
export const merged = a.extend(b).pipe(sum);
TODO
From the quiver
team:
TODO
From the community:
TODO
TODO
Check out these runnable examples:
- Hello, world!
- Using quiver with React
- ENS authentication
- Peer-to-peer, serverless Tic-Tac-Toe
- Type-Safety
If you have a use-case in mind, and are wondering how it might work, don't hesitate to open an issue, join the discord, or DM @killthebuddha_ on X.
TODO
Quiver
is built on top of the superb XMTP messaging protocol. XMTP provides out-of-the-box end-to-end encrypted messaging.
TODO
Right now we're currently on the path to v0.
If you have a feature (or bugfix) request, don't hesitate to open an issue or DM @killthebuddha_ on X.