How to Build a High-Performance App With Prisma and Replicache 🚀

Engineering
December 16, 2022
How to Build a High-Performance App With Prisma and Replicache 🚀

TL;DR

Stack: Prisma, Replicache, React, NextJS, PlanetScale DB
Demo: https://prisma-replicache-demo.vercel.app/
Repo: https://github.com/simplerlist/prisma-replicache-demo

Summary

In this article, we're going to show you how to create a high-performance app using Replicache and Prisma. Both are massively impactful technologies that make building high-performance applications easier than ever.

Prisma makes working with databases easy, especially for full-stack developers (no SQL coding required), while Replicache makes the entire user experience seamless and allows you to create an offline- and multiplayer-capable app for almost any platform! After integrating Prisma and Replicache into simpler (an AI productivity app 🚀), I’ve had customers say the app is blazing fast! 🎉 and a fantastic tool. So full-stack developers have a lot to gain from integrating both of these technologies into their SaaS apps.

Both Prisma and Replicache are designed to work with almost any backend stack that meets certain minimum requirements, so there’s a lot of flexibility here. For example, in this demo I’ve used PlanetScale, a MySQL database (which, by the way, is super fast!), but Prisma and Replicache can also work with other databases. In my view, when you combine these two powerful technologies, an incredible user experience is created. Because I haven’t found any documentation on how to integrate Prisma and Replicache, I wrote this article and built this demo to assist others in their journey of integrating these two powerful tools.

We'll walk you through the steps required to integrate Replicache into your existing app, make changes to your Prisma data model, and then write some code that uses Replicache to synchronize your app's state with the cloud.

After completing this tutorial, you'll have a high-performance app with an instant UI. So let's get started!

Replicache Docs

While this article guides you through the intricacies of integrating Replicache with Prisma, it’s in no way a replacement of the Replicache Docs, which lay out in great detail how to install, use, and work with Replicache. I highly recommend starting out with the ”How Replicache Works” page.

A Note on Convention

This is a demo application written by myself (Martin Adams, founder of simpler), and thus it has a file structure and file naming convention that’s unique to how I code (I try to use clean code principles). You’re welcome to adopt these conventions or use different conventions entirely — for the purposes of integrating Replicache, it won’t matter at all.

Differences Between Replicache and This Demo

There are some important differences between how variables are used in the Replicache docs and this demo that need to be pointed out upfront: Replicache uses clientID (referred to in this demo as replicacheClientId), as well as cookie (referred to in this demo as version). Also, Replicache uppercases ID in its variable names, whereas this demo doesn’t: lastMutationID in this demo is lastMutationId, and spaceID is spaceId in this demo.

1. Download the Repository

The first step is to download the repository from GitHub. This will give you all the code necessary for your app, as well as a demo server that you can use to test it out.

git clone https://github.com/simplerlist/prisma-replicache-demo.git
cd prisma-replicache-demo
yarn install

You’ll notice there is a .env file that contains all of the test passwords you need. However, the .env file still needs the correct PlanetScale database password. Since we’re using a test database, I’ve included the password string in the README.md file. You’ll need to paste that string into the .env file in order to work with the test database. However, please do not commit this updated .env file into a public repository, since doing so will immediately invalidate the PlanetScale test database password.

2. How Does Replicache Work?

Replicache has a succinct explanation on its website, reproduced here for simplicity:

Replicache data is divided into spaces up to 64MB in size. When a user first navigates to a space, Replicache downloads the data and stores it persistently in the browser.

The application reads and writes only to its local copy of the data. Thus the application responds instantly to all interaction by default.

Replicache synchronizes changes to the server and other clients continuously in the background. Users see each others’ changes live.

Conflicts happen if two users edit data concurrently. Replicache merges conflicts using a form of server reconciliation — an intuitive and powerful technique from multiplayer games.

If sync can’t happen because the server is down or there’s no network, changes are queued persistently until the server comes back. Replicache apps can smoothly transit online, offline, and unreliable networks.

3. The Prisma Schema

Next, check out the Prisma Schema. You’ll notice four distinct tables: ReplicacheClient, Space, Todo, and User. Since this demo is a to-do app, the Todo and User models are being used here to model the domain of the app.

What’s the Space table for?

The Space table relates to a group of data connected to a user. In our to-do example, a user’s private to-do list is one space, and a shared to-do list is another space entirely. This is important because we don’t want the data related to our shared to-do list to interact in any way with the data related to our private to-do list. A space, in turn, can be composed of a set of different kinds of data, such as to-dos, categories, tags, etc.

So one user can have one or more spaces, but each space has to be connected to one user. Correspondingly, in our to-do app the user’s can’t have tasks, but spaces can. In our Prisma schema, you’ll see, then, that the Todo table isn’t connected to the User, but to the Space table, which, in turn, is connected to the User. To learn more about Spaces, head on over to the Replicache Docs here.

And here’s why the concept of a “space” is so important: Replicache tracks all changes per space. So if you make changes to your personal to-do list, but then make a change in your shared to-do list as well, those two changes are tracked separately. To accomplish this, the Space table has a spaceId field to keep spaces unique (this can be any unique value you like), as well as a versionAt field, which is an incrementing number for each change that was made to the overall dataset in each space.

What does the ReplicacheClient table do?

The ReplicacheClient table tracks what mutations were done by what “client”. A client is a particular browser instance that’s either pulling data from or pushing data to the server. When you reload the browser, open a new tab, or use a different device, a new Replicache client is created.

The ReplicacheClient table has a unique replicacheClientId field that is given a value (a UUID) by the Replicache engine each time a new client is created. Every time a particular client makes a change to a space’s dataset, we save an incrementing lastMutationId to the ReplicacheClient table.

How’s a mutation ID different from a version?

The lastMutationId is not the same as the versionAt field in the Space table. The lastMutationId is just a counter that increments each time a particular client makes a change, while the versionAt field in the Space table is an incrementing number of all the changes made to the entire dataset for a particular space.

4. Overall App Structure

We have our folder structure organized like this:

/pages — all files accessible by the browser
/hooks
— hook files accessed by a page
/mutations
— client-side mutations done by Replicache
/pages/api/
— API routes
/utils
— various utility files accessed by the app
/utils/api
— various utility files exclusively accessed by the API routes

5. Login

To keep things simple, we’re not using an authentication system in this demo, so pressing the login button simply creates a new user and saves that information via a cookie. We’re also generating two random space IDs so you can see how switching to another space has no impact on the data of the first space.

6. Replicache Client

The main page is our /pages/index.js file. Here, we see Replicache being instantiated for each user and space inside the useReplicache file.

The Replicache constructor requires both name and licenseKey (click here to get a license). Instances that have the same name share storage in the browser — which is why the name is made up of both the userId and the spaceId in order to keep the data separate for each user and each space.

7. Client Mutations

Mutations are actions done by the Replicache client that will change some data on the server. For example, adding a new task, updating a task, or deleting a task altogether would all be considered mutations. To accomplish this, we have created four different mutation files that can be found in /mutations/todo. Anytime a user modifies data in the client, one of these mutators gets called.

Once Replicache has been instantiated, these mutations can be accessed via rep.mutate followed by the property name we’ve assigned to the mutators object in the Replicache constructor (so it can be any name you want).

Anytime a mutation is performed in the browser, the corresponding mutation file is called — so in our example above, the mutations/todo/create file is called.

Replicache has some conventions regarding these mutations, so it’s best to look it up in their docs how mutations are processed in the client.

Mutators are fast: https://doc.replicache.dev/tutorial/adding-mutators

8. Push API

Let’s head on over to the push API route. This is where the magic happens! ✨ For ease of use, I’ve broken down what happens inside the API route into smaller steps, and each step consists of separate files.

Prisma Interactive Transactions

Notice the isolationLevel being set for Prisma’s Interactive Transaction

Replicache requires transactions to be run at the serializable isolation level in order for sync to work properly. We accomplish this in PlanetScale using Prisma Interactive Transactions and setting the isolationLevel in both the push and pull API routes. Prisma Interactive Transaction are a critically important feature for our Replicache integration, since if a mutation breaks, we’ll need to roll back any changes we’ve made to our ReplicacheClient and Space table as part of that transaction.

An important thing to note is that Replicache uses serializable transactions. That means that as various mutations are applied by various clients, the versioning is tracked in increments of 1.

Step #1. Get next version for space

Since Replicache uses a versioning system to keep track of changes, similarly to git, it needs a place to save the latest version. So the first step is for us to get that version via the versionGet file and then increment it by 1 for the upcoming mutation. An important thing to note is that since the spaceId is merely provided as a query to the API route, anyone can modify that value, so it’s critically important to also include the secure userId in the space query.

Step #2. Get last mutation ID for the client

Here, in the lastMutationIdGet file, we’re going to get the last saved mutation ID for the Replicache client that’s attempting to perform the mutation. If it’s the first time the Replicache client is attempting to perform a mutation, we’ll need to create a record in the ReplicacheClient table with a lastMutationId of 0.

Step #3a. Cycle through each mutation

In the mutations file we’re cycling through each mutation (a Replicache client can batch several mutations together and send them through as an array), and increment the mutation ID by 1 after each successful mutation.

Importantly, we’re assigning the next version to the individual record in the field versionUpdatedAt. This is a critically important step.

Step #3b. Associate each updated record with the next version

Whether a record is being updated, created, or deleted, a the next version has to be associated to that record via the versionUpdatedAt field. This will be relevant in the pull API.

Step 4. Save incremented mutation ID

In the lastMutationIdSave file we save that incremented mutation ID in the ReplicacheClient table.  This number, too, will be relevant in the pull API.

Step 5. Save incremented version

Finally, we’ll need to save the incremented version in the versionSave file, which will be relevant for the next push transaction. Remember, the lastMutationId will have been incremented by however many mutations were performed by a particular browser instance, while the version was incremented only by 1 during the entire push transaction.

Websockets

At this point, the Prisma Interactive Transaction has finished, and we’re now sending a “poke” from the send file to the Replicache client instructing it to initiate a pull and get the latest authoritative data from the server.

9. Pull

Over at the pull API route, we once again initiate a Prisma Interactive Transaction. This route is broken into four steps, and these steps are easier to summarize: In step #1. and #2. we get the version and lastMutationId (like in the push API). In step #3., in the todoGet file, we’re getting the data that’s been updated since the last push, while in step #4. we’re compiling that information for the Replicache client.

10. Client Subscription

The updated data from the pull then updates the client data via Replicache’s subscription model.

Summary

And there you have it! Our Prisma-Replicache integration is now complete and our app is now high-performance, offline- and multiplayer-capable. So go ahead build something amazing! Good luck and happy coding!

Martin Adams
founder, simpler. 🚀

Get things done better with simpler.

simpler is a smart productivity app that helps you organize and prioritize what needs to get done so you can focus on what truly matters.

Join!