Skip to content

Dead-simple, secure, type-safe RPC. The quickest way to deploy a JavaScript function to the internet.

License

Notifications You must be signed in to change notification settings

killthebuddh4/quiver

Repository files navigation

Quiver logo

Quiver


License Created by Achilles Schmelzer stars

a dead-simple 😵, secure 🔐, type-safe 🦄 RPC client and server
powered by the XMTP messaging protocol.

Documentation   •   Discord   •   npm   •   Issues   •   @killthebuddha_   •   fig

Demo

Quickstart

npm install @qrpc/quiver or yarn add @qrpc/quiver or pnpm add @qrpc/quiver

  1. Serve a function.
  2. Call the function.
  3. 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.

Table of Contents

Features

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

Basic Usage

Functions

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);
});

Routers

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" }

Middleware and Context

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.

TypeScript!

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!

XMTP Network Client

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 }

Middleware Guide

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)

Merging Middleware

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);

API Reference

TODO

Quiver

QuiverRouter

QuiverFunction

QuiverClient

QuiverMiddleware

QuiverProvider

Off-the-Shelf Middlewares

From the quiver team:

TODO

From the community:

TODO

Advanced Examples

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.

Under the Hood

TODO

Quiver is built on top of the superb XMTP messaging protocol. XMTP provides out-of-the-box end-to-end encrypted messaging.

Roadmap

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.

About

Dead-simple, secure, type-safe RPC. The quickest way to deploy a JavaScript function to the internet.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published