tRPC integrations for CRDTs: CRDT-native RPC calls
For when you're building with CRDTs but you want to run code on a server.
NPM packages are available for the following CRDT systems:
- Yjs
npm install trpc-yjs
- example app
- ElectricSQL
npm install trpc-electric-sql
- example app
In progress
Please PR additional integrations — the goal is to support all CRDT implementations.
Run server functions from the client that write to replicated data structures.
// Run a job
await trpc.aiSummarizationJob.mutate({ id })
console.log(jobResults.get(id))
// Schedule an event
const eventId = await trpc.scheduleRoom.mutate({
date: `2023-11-04`,
startTime: 12,
endTime: 13
})
console.log(events.get(eventId))
A simple Yjs implementation (see the examples directory for full examples for each integration).
import * as Y from "yjs"
import { createTRPCProxyClient } from "@trpc/client"
import { link } from "trpc-yjs/link"
// Doc needs replicated via a server e.g. with y-websocket.
const doc = new Y.Doc()
const trpc = createTRPCProxyClient<AppRouter>({
links: [
link({
doc,
}),
],
})
// Tell server to create a new user.
await trpc.userCreate.mutate({id: `1`, name: `Kyle Mathews`})
// The new user, written by the server, is now available at
doc.getMap(`users`).get(`1`)
import { adapter } from "trpc-yjs/adapter"
import { initTRPC, TRPCError } from "@trpc/server"
import { z } from "zod"
const t = initTRPC.create()
const router = t.router
const publicProcedure = t.procedure
const appRouter = router({
userCreate: publicProcedure
.input(z.object({ name: z.string(), id: z.string() }))
.mutation(async (opts) => {
const {
input,
ctx: { users },
} = opts
const user = { ..input }
// Set new user on the Y.Map users.
users.set(user.id, user)
return `ok`
})
})
// Get replicated Yjs doc to listen for new tRPC calls.
const doc = getYDoc(`doc`)
// Setup trpc-yjs adapter.
adapter({ appRouter, context: { doc, users: doc.getMap(`users`) } })
CRDTs enable local-first style development — local-first is a software architecture which shifts reads/writes to an embedded database in each client. Local writes are replicated between clients by a Sync Engine.
The benefits are multiple:
- Simplified state management for developers
- Built-in support for real-time sync, offline usage, and multiplayer collaborative features
- Faster (60 FPS) CRUD
- More robust applications for end-users
Read my longer write-up on why I think local-first is the future of app development: https://bricolage.io/some-notes-on-local-first-development/
But not everything is cute puppies and warm bread and butter in CRDT-land.
CRDTs (and local writes) are amazing until... you need a server (it happens).
- the code requires specialized hardware not available in the client
- the code is written in a non-javascript language (it happens)
- the code needs to talk to 3rd party API with restricted access
- the code needs more resources to run than are available on the client (common with mobile)
- the code needs data that's not available on the client (data doesn't fit or too expensive to load)
CRDTs make optimistic client mutations far safer than normal but an authoritative server is still often needed:
- mutations that need complex or secure validation (e.g. money transfer)
- mutations that include writes to external systems that must be completed in the same transaction e.g. writes to a 3rd party API
- mutations to complex data that's not easily expressed in CRDTs
- mutations against limited resources e.g. reserving a ticket to a show
For each of these, the good ol' request/response RPC pattern is a lot easier and safer than optimistic client writes.
You might be wandering: why not just write normal tRPC (or REST/GraphQL) API calls?
This is possible but there are some potent advantages to keeping everything in CRDT-land:
- No need for client-side state invalidation/refetching after server writes. Writes by the server during a tRPC mutations are pushed to all clients by the sync engine. Data across your component tree will be updated simultaneously along with your UI — a major pain point for normal API mutations!
- RPC calls get all the benefits of of CRDTs:
- server calls over CRDTs are resilient to network glitches with guaranteed exactly-once delivery. No need to add retry logic to your calls.
- RPC calls are now replicated (if you choose) in real-time to other users of the application
- Simplify your architecture. If you're using CRDTs extensively in your applications, tRPC over CRDT helps keep your architecture simple and consistent.
- A free audit log! Which may or may not be useful but it can be handy or even essential to see a log of all mutations.
- Easy real-time updates for long-running requests e.g. the server can simply update a progress percentage or what step the request is on. If some requests are of interest to a wider audience e.g. in group collaboration, running requests over CRDT means you get free real-time job notifications.
This library and local-first in general is very early so there's lots of ideas to explore and code to write.